From e4e782f274d178f2de802eff8b8e7d7ca5506be0 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:25 +0000 Subject: [PATCH 01/29] build: add the Cargo workspace, Python packaging and fensu policy Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .agents/setup | 51 ++ .gitignore | 25 + .python-version | 1 + Cargo.lock | 824 ++++++++++++++++++++++++++ Cargo.toml | 59 ++ LICENSE | 201 +++++++ fensu.toml | 49 ++ packages/sqlclean-cli/README.md | 3 + packages/sqlclean-cli/pyproject.toml | 36 ++ pyproject.toml | 83 +++ uv.lock | 831 +++++++++++++++++++++++++++ 11 files changed, 2163 insertions(+) create mode 100755 .agents/setup create mode 100644 .gitignore create mode 100644 .python-version create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 LICENSE create mode 100644 fensu.toml create mode 100644 packages/sqlclean-cli/README.md create mode 100644 packages/sqlclean-cli/pyproject.toml create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.agents/setup b/.agents/setup new file mode 100755 index 0000000..a6b511e --- /dev/null +++ b/.agents/setup @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# Prepares a fresh Amp orb: Rust stable, Python 3.12 and the locked uv environment with the +# native sqlclean executable built. Idempotent; a warm run on a restored snapshot only re-verifies. +set -euo pipefail + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +step() { + local name="$1" + shift + local start=$SECONDS + echo "==> ${name}" + "$@" + echo "<== ${name} ($((SECONDS - start))s)" +} + +# One persistent Cargo target directory per worktree, outside the repository. +target_dir="$HOME/.cache/cargo-target/$(basename "$repo_root")" +export CARGO_TARGET_DIR="$target_dir" +export PATH="$HOME/.cargo/bin:$PATH" + +install_rust() { + if ! command -v rustup >/dev/null 2>&1; then + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \ + --component clippy --component rustfmt + else + rustup update stable --no-self-update + rustup component add clippy rustfmt --toolchain stable + fi +} + +persist_env() { + local marker="# sqlclean orb environment (${repo_root})" + if ! grep -Fqx "$marker" "$HOME/.bash_profile" 2>/dev/null; then + cat >>"$HOME/.bash_profile" <=1.9,<2"] +build-backend = "maturin" + +[project] +name = "sqlclean-cli" +version = "0.0.1" # x-release-please-version +description = "Native command-line executable for SQLClean" +readme = "README.md" +requires-python = ">=3.12" +license = "Apache-2.0" +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Rust", + "Topic :: Software Development :: Quality Assurance", +] + +[project.urls] +Homepage = "https://github.com/sqlstack-io/sqlclean" +Issues = "https://github.com/sqlstack-io/sqlclean/issues" +Repository = "https://github.com/sqlstack-io/sqlclean" + +[tool.maturin] +bindings = "bin" +manifest-path = "../../crates/sqlclean-cli/Cargo.toml" + +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, + { file = "../../Cargo.toml" }, + { file = "../../Cargo.lock" }, + { file = "../../crates/*/Cargo.toml" }, + { file = "../../crates/*/src/**/*.rs" }, +] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..7ab39a3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,83 @@ +[build-system] +requires = ["hatchling>=1.27"] +build-backend = "hatchling.build" + +[project] +name = "sqlclean" +version = "0.0.1" +description = "Fast, opinionated SQL linting and deterministic formatting, with custom Rules in Python" +readme = "README.md" +requires-python = ">=3.12" +license = "Apache-2.0" +license-files = ["LICENSE"] +keywords = ["sql", "lint", "linter", "formatter", "duckdb", "snowflake"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Programming Language :: SQL", + "Topic :: Software Development :: Quality Assurance", +] +dependencies = [ + "sqlclean-cli==0.0.1", # x-release-please-version +] + +[project.optional-dependencies] +ast = [ + "polyglot-sql-chio==0.13.10", +] + +[project.urls] +Homepage = "https://github.com/sqlstack-io/sqlclean" +Issues = "https://github.com/sqlstack-io/sqlclean/issues" +Repository = "https://github.com/sqlstack-io/sqlclean" + +[tool.hatch.build.targets.wheel] +packages = ["src/sqlclean"] + +[dependency-groups] +dev = [ + "commitizen>=4.9.1", + "fensu>=0.28.0", + "pre-commit>=4.3.0", + "pytest>=8.4.2", + "pytest-xdist>=3.8.0", + "ruff>=0.12.0", + "ty>=0.0.1a19", +] + +[tool.uv] +cache-keys = [ + { file = "pyproject.toml" }, +] + +[tool.uv.sources] +sqlclean-cli = { path = "packages/sqlclean-cli" } + +[tool.pytest.ini_options] +testpaths = ["tests"] +addopts = ["--import-mode=importlib"] +pythonpath = ["."] + +[tool.ruff] +target-version = "py312" +line-length = 100 + +[tool.ruff.lint] +select = ["B", "E", "F", "I", "PLR0913", "PLR0915", "UP"] + +[tool.ruff.lint.pylint] +max-args = 10 +max-statements = 70 + +[tool.ruff.lint.per-file-ignores] +"tests/**/test_*.py" = ["E501"] + +[tool.commitizen] +name = "cz_conventional_commits" +version_provider = "pep621" +version_scheme = "semver2" +update_changelog_on_bump = false diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..b84f833 --- /dev/null +++ b/uv.lock @@ -0,0 +1,831 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" + +[[package]] +name = "argcomplete" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/87/6f/5a73f04007ca950701765949209f068da628bd11f9c2da287278ce91e0ee/argcomplete-3.7.2.tar.gz", hash = "sha256:aad8b69a0b9969edb62db0d1752354c0d50717b10e0cbb00e2a958381b9fc6b9", size = 74473, upload-time = "2026-08-06T04:53:21.662Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/bd/551ee6af426af84ca33e02622be722925c196608e9127d731ef17c47f06e/argcomplete-3.7.2-py3-none-any.whl", hash = "sha256:6029205678bdd9c1c728a155f5f9ecf5812393f969eef58807641a2bc2aa5b19", size = 43294, upload-time = "2026-08-06T04:53:20.246Z" }, +] + +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/33/1c/f41d4e74c28ab327ff3acd36053f7ea506c55872d7a90b0fa71aa3ab0c89/charset_normalizer-3.5.2.tar.gz", hash = "sha256:39de2a259fc954455c57274dc94c79d5842774e1247a016aff30bc0efed0f4ef", size = 172659, upload-time = "2026-09-30T04:39:23.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/c8/693809898870237d82785a03f3b2b58fe4c9f14669f84a7d4e623c92a59e/charset_normalizer-3.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ed2a239c0ea213acc1908150a3037257083c7c083128f1a4cec2ec4b97dca491", size = 367780, upload-time = "2026-09-30T04:35:30.888Z" }, + { url = "https://files.pythonhosted.org/packages/c9/87/2fea8c13dc24b3ca9c6f803a5b2dfdeae73eb4f9e12c7885ed908ff0433c/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b91363207bd9dc966a691e959bb47f64b30f7ac4b072be9968b366982f7db77c", size = 246730, upload-time = "2026-09-30T04:35:32.286Z" }, + { url = "https://files.pythonhosted.org/packages/a8/9e/09efac30b937722f46d3110ba30b875b24b2e3a266ed746cc4e376a94d80/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38a873987f3be698494da8b2e3085e29da02da7b633dce73e79c699a113d7bf0", size = 237707, upload-time = "2026-09-30T04:35:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/18/70d76670b13686237863a379928d60bd10e021f17d243ab3d7014c4a5f4e/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:355ad8011081dec5412240c087a9a0c9d4d5039f3ed11a3f13e18c2b29b56c51", size = 273050, upload-time = "2026-09-30T04:35:35.138Z" }, + { url = "https://files.pythonhosted.org/packages/54/e2/77a8b09d5adc013ed07b95b01b8b8fa5441c4e810e83ee7e4aae2fa4d91a/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ee21e28f0430bd6dc9086c6e525d5e818a44a5ad19720c8a0ef766792f3eb5e5", size = 270345, upload-time = "2026-09-30T04:35:36.502Z" }, + { url = "https://files.pythonhosted.org/packages/7f/c5/38806a25ab5e65fc178f39affeda20858efafede2fce1ffc2556cfc9fe73/charset_normalizer-3.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d31298449090ab8d47b7b1b2a555ff73cac7ed438a08b7ac160980c7ebed649", size = 257601, upload-time = "2026-09-30T04:35:37.919Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/213565184708fdb263ae55e2c04ee1ff748129dd65d48ed0e3502da9c85a/charset_normalizer-3.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5cde776b7cc66e4f6c99612cea4aa7269aa65863f7a15841b2c264f103822f4e", size = 252222, upload-time = "2026-09-30T04:35:39.544Z" }, + { url = "https://files.pythonhosted.org/packages/7e/24/76d2cefc25472531e4c5c7dfff68865eb1c39b78482f0fdc15b46f047830/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ae4f5fea5b8b8ccff88238cc8569303e5ee95efae67fa62922a311397a71f346", size = 248482, upload-time = "2026-09-30T04:35:41.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/dc/65a801b66ab4c197e22c433ab25e7ac24324ac6f45a2269aca42cce309bf/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:f7d486c83842422badd511868fd8a9a20e9407ace71564b6af47ce7e60a336c1", size = 241206, upload-time = "2026-09-30T04:35:42.59Z" }, + { url = "https://files.pythonhosted.org/packages/a7/95/ca9b5eabde673002c6f1e7ada1b223916fe18f6d661da7aabd4d643718f1/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:11a4d68a6ecda3292cb1e50239e111543ba5d709bb62a6b4ea1afcfa729d8875", size = 273190, upload-time = "2026-09-30T04:35:44.347Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8b/803b4d2a3f6e1740f63f1e87b04d14b42f3d4fdfe6ed7d4db2d34102b14f/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d6734d2ef8a50fbf8445c139477da401f50d62a0606bf00e20ec6d87773fefb1", size = 253527, upload-time = "2026-09-30T04:35:45.915Z" }, + { url = "https://files.pythonhosted.org/packages/a9/55/93c0e5dbd085ae0471346026abbe7e0db9ea2d6fea74e51f0b5a46f233a7/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a815775b6c38d4e0ff7bcffbeba67feded90202bb6a226b8dd35f1c855217413", size = 271285, upload-time = "2026-09-30T04:35:47.49Z" }, + { url = "https://files.pythonhosted.org/packages/95/69/0dbd0e0b9b16cfa816cdfcb3e2e3854a1f680dc07fb1245ea125e7448060/charset_normalizer-3.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:23851fb4e1b85ed3f6c2a27b777cdfe2e19fb5b38429a8faf38c7542b7665869", size = 260010, upload-time = "2026-09-30T04:35:48.996Z" }, + { url = "https://files.pythonhosted.org/packages/58/9d/e7b88e7b1bf403590c3b573277b5e1e488c68c7a6fbacca310a2c324e90c/charset_normalizer-3.5.2-cp312-cp312-win32.whl", hash = "sha256:db19d07e2e0129e974a0e65d0064fc222a446cd5122c2fd4184d2af9fc734a9e", size = 184125, upload-time = "2026-09-30T04:35:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e6/e6e083884cbcfd49c64865af05027fe7011be7b2d9179524f099a1b611f3/charset_normalizer-3.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:780fbe7cab297b81dad9fb8dc5eb003c0468ffb0d9e5f65068c53a34661a96bc", size = 207486, upload-time = "2026-09-30T04:35:52.194Z" }, + { url = "https://files.pythonhosted.org/packages/c4/e3/017aea0911ada7405a825c7d937eb3a13009664e2f5b38e8c4bbf2abf894/charset_normalizer-3.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:e2af3aad578aa6bd1384bcf4750fc285e5a9de53f40b7d41e5a0bf748edeb2b3", size = 196734, upload-time = "2026-09-30T04:35:53.636Z" }, + { url = "https://files.pythonhosted.org/packages/c5/34/68292d68512768591aaff07c59bb53ee31341c87759433a859c4641a50c2/charset_normalizer-3.5.2-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:ed905975ab14056a2e5eb1c376cb2e1ebc5396baf84163939c518556fccde9f5", size = 225946, upload-time = "2026-09-30T04:35:55.313Z" }, + { url = "https://files.pythonhosted.org/packages/e3/80/bee0b01b90ccd5322ae1d0abb33fab1bd95b7c2eadaf02aeccf22e04ee83/charset_normalizer-3.5.2-cp313-cp313-android_24_x86_64.whl", hash = "sha256:a66c3bc5ab1f0ff2164fc9965ddd611ff0802173f4b9d24554c563f6ab7e1d6e", size = 238619, upload-time = "2026-09-30T04:35:56.863Z" }, + { url = "https://files.pythonhosted.org/packages/78/6e/60ce52a85a7fd631ae8482ae6d74521014ca2f255892679484dc04d7ef56/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d2374b62878abb00cd8309b32af6c0b715cd02dec0ca74ef12e5069bdc64144a", size = 206701, upload-time = "2026-09-30T04:35:58.639Z" }, + { url = "https://files.pythonhosted.org/packages/36/8c/71aafad23f971afc84c2b295bc0c560739ce1dac558aad9fec22e39f3639/charset_normalizer-3.5.2-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:d376bbd28b3a8999db1a103b3b388aee6f1ddeb3e51bc2172993efdcd86e064d", size = 210506, upload-time = "2026-09-30T04:36:00.147Z" }, + { url = "https://files.pythonhosted.org/packages/91/da/3c5a7798c046df7d2d68ad653cf5b6c5a8bfee225055a843c6f2f42aac1a/charset_normalizer-3.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6045373d5a89a5ec71afde535db987ca28e76dfa276c2d4c818265b375d4b055", size = 366626, upload-time = "2026-09-30T04:36:01.77Z" }, + { url = "https://files.pythonhosted.org/packages/e1/16/710ac3de2ee354e2bd1a9c94efe45a2d27b5c6ad39b2d6a905be2c094b6c/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:849df64e889b2e17230d58410a03dba311a65b163508fd33679b2b737d4b7858", size = 244624, upload-time = "2026-09-30T04:36:03.389Z" }, + { url = "https://files.pythonhosted.org/packages/d6/39/45c7439f5b63d24f7d5b2a1d760f34af7628782d7144b4cc8ded45c2d4bc/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:15c44f7edfd477b06f517a5cc317fc1707edb9de2c865f43d4b6513907473234", size = 235731, upload-time = "2026-09-30T04:36:04.987Z" }, + { url = "https://files.pythonhosted.org/packages/4d/34/38f3154785ce92e9f56eb226f4d35bdfae6b008480dd055f58837a89c810/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a89012d6d5476ee112d20d998570ed58df2260a852afb1758809cd6900411d21", size = 270352, upload-time = "2026-09-30T04:36:06.412Z" }, + { url = "https://files.pythonhosted.org/packages/04/f3/859f74e7babc977705026b30593b3be04049632a522fb7000f83c033d747/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0c951d5e6dd9c2ff60609476752bee49da4206adde960ebc247766937f72e718", size = 267393, upload-time = "2026-09-30T04:36:07.865Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/41d27f234b82e47c167a5f6c0f62501dc0c640585ff4aba79e08a390336a/charset_normalizer-3.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7218e8f32b0956cfcd048fd42d9d5779809745ca1d86113ca56f66e7ae1549c4", size = 254660, upload-time = "2026-09-30T04:36:09.248Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/5d1a997587febe5b26d8daffe363b5c1a091cece19828eec6502fd09c5ef/charset_normalizer-3.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a19a731138fc27d5682277d3b9df22855cea1239bce7fcec5f78f42ef2d1f3c3", size = 249996, upload-time = "2026-09-30T04:36:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/b3/1f/d1e78246f7ed60c8c8d606b4ac27f66ce49cc3e95f24893ccbeba9f77302/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:62603db9a7caa0802eaa28c1c46fecd7b3a263a774069c24c3c28c302448721c", size = 247028, upload-time = "2026-09-30T04:36:12.294Z" }, + { url = "https://files.pythonhosted.org/packages/8e/37/eba316edd4f0c4d3a5d945924c4eeeae59abac4056aa815d8a4268f863a2/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:b6856554c4f44d79fc2307d5768854310a8f0096e501c75637542c82292b0429", size = 240562, upload-time = "2026-09-30T04:36:13.887Z" }, + { url = "https://files.pythonhosted.org/packages/c8/8e/aaa037d40ca9ef045977f1a661048b1aa33f223adfce3452fe9be9f79d14/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:1bc0baf5ef96b6ede57d47f4b8fe4d9d84019c3bfcbeb20a41edc6a6ee341f1f", size = 271699, upload-time = "2026-09-30T04:36:15.41Z" }, + { url = "https://files.pythonhosted.org/packages/26/19/1c1c9f75974adf523b87f34b8a2adc5a435cd65916812bcbd0dfa45f9a29/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:56bc200a365efb37383b7852e4cc5898d3b2da5987289b543956cf8cad71018a", size = 252030, upload-time = "2026-09-30T04:36:16.839Z" }, + { url = "https://files.pythonhosted.org/packages/bc/90/0660ef18e18df0a4d2a1a0edff7dfbba42d4e50ef2425557a5bb7051f77b/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2c9ad19a6cfcd5ea5c0d41161d22f9df1dcc277e9bef2751391334546a314c00", size = 268565, upload-time = "2026-09-30T04:36:18.468Z" }, + { url = "https://files.pythonhosted.org/packages/79/ba/57adc269824e8658f1a0f97a9e514c247445a9632b3419b97e0ba37f16dc/charset_normalizer-3.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e243bd13217235fc7290c621941c3f5cc8b66e4872495be821d7436ba2fb838d", size = 257627, upload-time = "2026-09-30T04:36:19.938Z" }, + { url = "https://files.pythonhosted.org/packages/9a/85/33abd4315c052d3d4f54c92b1ee49bfbc0dc7115a981e462a793b6d2ab87/charset_normalizer-3.5.2-cp313-cp313-pyemscripten_2025_0_wasm32.whl", hash = "sha256:a090bb2c68df85450502e3e20d665e3a5af9c65a84d6508ed477badd49166fd3", size = 143407, upload-time = "2026-09-30T04:36:21.376Z" }, + { url = "https://files.pythonhosted.org/packages/4f/de/6435e18d1aaa5d910b896d551411c96af1f42a0c56c29afc2016c61ccc2e/charset_normalizer-3.5.2-cp313-cp313-win32.whl", hash = "sha256:2b7b3bbfb4fe8ef40600792d762fbaa9057559f9d3fad209525b7a22b99e91fd", size = 183207, upload-time = "2026-09-30T04:36:22.776Z" }, + { url = "https://files.pythonhosted.org/packages/9c/76/b8ec57f4e9ee3253541abf95e4a462c0175fe8032dcd070f1f2421240942/charset_normalizer-3.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:78456a747de8dc58360ffa581f30a002baf5aa28cb262536545e91f113ed7639", size = 206489, upload-time = "2026-09-30T04:36:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/3e/60/c647c6ae47480221e875ea5d743ff94946f7416e3c69415ab772928e8d32/charset_normalizer-3.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:11912e4bb14baae7c5d8791aa55ba0a3a03ec6729073307b0f57270abaa713d3", size = 195501, upload-time = "2026-09-30T04:36:25.846Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/7aa91362a2f77ac8e9e28a9b902a74f7d0e11a851ef0d27a74308da8cd90/charset_normalizer-3.5.2-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:1afb975bd5d68d5ce9f6b6d44fdf2f7e34b895a35e95708a7a91b20a3b51d187", size = 226010, upload-time = "2026-09-30T04:36:27.669Z" }, + { url = "https://files.pythonhosted.org/packages/a8/cf/ac8878d0322cf88a1aad4c7b147db32ca0bd806eb0060957b2e31486dbe6/charset_normalizer-3.5.2-cp314-cp314-android_24_x86_64.whl", hash = "sha256:bbbfc8e28816f19d7c0f1816664980c0a9875d01b27cdf8eedddb639d9e108ad", size = 239266, upload-time = "2026-09-30T04:36:29.434Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6d/9a08d7e0b29b7208e2c6c01dc56c8e0520e7c7beadbbfb024b58fd69c8a5/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:7967d08cf06dee78443b874f98c98036f624f3a4e73e11f9f64f5be4d25393cf", size = 206783, upload-time = "2026-09-30T04:36:30.872Z" }, + { url = "https://files.pythonhosted.org/packages/82/44/b0aa350280e6ff5a5492d17cf10460dd39d5ee848f872f7ba2df10607f60/charset_normalizer-3.5.2-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4c2b5031f63e331e3839b40aed2dd6f191e9c07edbde303e7876846ea1946995", size = 210449, upload-time = "2026-09-30T04:36:32.625Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8a/40db9aa9f5907bb0e6f8b6d64064bf8852fb33d4b813ff9414911df7647c/charset_normalizer-3.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fcff63213e8e6e47770541a4607175404f47cbb3ebea7b6058cc82d524a0e424", size = 367815, upload-time = "2026-09-30T04:36:34.197Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/9c5e7707b57c8ddfa9ddf7b0b1d009d7fbab9e9e887d5b721060f37e307d/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d86d6fc60743dc916eb79e2eb1ec4818e21e427731543af40a3021851174a13", size = 247095, upload-time = "2026-09-30T04:36:35.803Z" }, + { url = "https://files.pythonhosted.org/packages/83/09/71e453691e927de4ddf792770cfaab3f49d494e222f66ea5e404bbd5e39c/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7a881931aa470808df94a8c380eed2bbbc76cd9dc622310f99665658c821eb6d", size = 233942, upload-time = "2026-09-30T04:36:37.407Z" }, + { url = "https://files.pythonhosted.org/packages/9f/86/85c84e4da8b27dd409577d9437926ff581c5f9d3c66038dc68c1a526de51/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8024d00c3faf3fc0c16e07a69f4405e8eac7cc0ab15f65fe6cf43827c4cf72b4", size = 273762, upload-time = "2026-09-30T04:36:38.904Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/564955a4b5f2ccb410ab480bbe8c6a18063ff27f2d35458731c4a5335df9/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4d48f2d08b9de5864e2c8744d4461b862fb149a18274abc8b698c45975573438", size = 269514, upload-time = "2026-09-30T04:36:40.469Z" }, + { url = "https://files.pythonhosted.org/packages/18/24/bad3ac4271589df29cf5ce2f5ae490518a5739358052bd0d61209e6fea54/charset_normalizer-3.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34276fd796040bf0993ab33a369aa572e6979c7aab225a88893667ad8eac8f7a", size = 255056, upload-time = "2026-09-30T04:36:42.02Z" }, + { url = "https://files.pythonhosted.org/packages/d6/3e/350d89ad49916b86554d6f5f2d03ec1152148f87e5ff735106c6a03b1a36/charset_normalizer-3.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0521c5665880b33d603717defa76c094048900010897909952397feb3039da56", size = 251311, upload-time = "2026-09-30T04:36:43.577Z" }, + { url = "https://files.pythonhosted.org/packages/56/5b/4970a2d154df502e133402906dd04e3ae7cada7b3011283c88d0479a2585/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:eff0ac9dbe711a4aee69bf04a83896aa9b85f19641264053a9f6d48573abb7dd", size = 250190, upload-time = "2026-09-30T04:36:45.185Z" }, + { url = "https://files.pythonhosted.org/packages/88/8c/f1a91bddc8fb47c2889e29ea7ea49a194eb0d9868675d786806519c00d76/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:1503bccbeb36d5527790c3930327704c39af22de3112f1b1666a9f3ce15ee204", size = 237098, upload-time = "2026-09-30T04:36:46.689Z" }, + { url = "https://files.pythonhosted.org/packages/24/0e/bb5dace3cc7e79068425386a6589c19b5a2ab5fefc2a46abea6919683332/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:52aa6992700996af31f375de0c6bacd402b0097fe40b53c426b9f51a90ebabc7", size = 274228, upload-time = "2026-09-30T04:36:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/9d/79/b849ad523017ea9f5a45581bbebed91439e0cf42fd2860a6f64e358eb5a6/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e09a3942ecbdee5cce73ea9d42da82b81b72ac1bf031ce069b93b5adf4eac8cd", size = 252401, upload-time = "2026-09-30T04:36:50.091Z" }, + { url = "https://files.pythonhosted.org/packages/89/8c/75469d690cf47200bce8f6cad7655724fc23148e147abfc5ce78b5f65863/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:c7c9ab723cde841fefb34efbad91e87f00a674b1fe1cd0784fde742bf2c154dc", size = 271846, upload-time = "2026-09-30T04:36:51.719Z" }, + { url = "https://files.pythonhosted.org/packages/26/cd/6d52d3c7437cdcf2e310ce9f28f282e733d4ef60ed19105d1819c356255f/charset_normalizer-3.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ddc7dacc8ece3a182e7f15cb862d1fd616b46d076cb1ae9dd232b2c38b655874", size = 258151, upload-time = "2026-09-30T04:36:53.234Z" }, + { url = "https://files.pythonhosted.org/packages/f7/4c/070b38bdb5f49a70199fce923ec0726a49536a63ab262abbfcaaf351110b/charset_normalizer-3.5.2-cp314-cp314-pyemscripten_2026_0_wasm32.whl", hash = "sha256:ee43c17b173d46a3212baa6ead3ae258eeabdae48c263a01ccf0218c366dd655", size = 143570, upload-time = "2026-09-30T04:36:54.816Z" }, + { url = "https://files.pythonhosted.org/packages/81/84/9ebfc8ed6c8c4fcd8e726ff6bf220cc8deb3966e31dce9be8dd8aa017e64/charset_normalizer-3.5.2-cp314-cp314-win32.whl", hash = "sha256:4f87960d57feabfb618e4e0af6e7371645fa26a277860739d6e5d6e0012c92f0", size = 186412, upload-time = "2026-09-30T04:36:56.643Z" }, + { url = "https://files.pythonhosted.org/packages/d1/78/5ed86f743d4bc350db307e7636419a0a5ee1d91806d30c7f667bd5c80dae/charset_normalizer-3.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:e4e81e09c1578b8df602e3db08b0b3ea0a6947ad612f52bf8dc5ea8d47691f0c", size = 210965, upload-time = "2026-09-30T04:36:58.205Z" }, + { url = "https://files.pythonhosted.org/packages/53/94/a3a7698e9b1a395e1eb99ccd9a324be9347973bff4e72db2a06496d7cd27/charset_normalizer-3.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:80d02b6f04e92601a081dd97b23d3128033098bff5d35d392ddcc0476ea11253", size = 200406, upload-time = "2026-09-30T04:36:59.764Z" }, + { url = "https://files.pythonhosted.org/packages/c1/48/c5dd00d5ef7791f02666de250a5bb6071e29b7e133cf4b835800b6d3bc27/charset_normalizer-3.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:dca9ab98072a5a54ebacebdc45f53e645336b320c667410b061be1ca588ae709", size = 396871, upload-time = "2026-09-30T04:37:01.543Z" }, + { url = "https://files.pythonhosted.org/packages/12/c8/8379554b42e8368161d898476686947a0fdbd3e8865170d7909dcabfdee8/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f0aa869112ef88429ae17820d99c3dd9504c9e9c671d3c246f3d7442cb051084", size = 249711, upload-time = "2026-09-30T04:37:03.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/eb/2ddb1035d17320caa9f41682935123a9a250277b261c3efc86b2d2a21343/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c0afc6800ba57ccc350374c5bd6150419915d95ce93cdbab2d783d75eaf30ecb", size = 231146, upload-time = "2026-09-30T04:37:04.721Z" }, + { url = "https://files.pythonhosted.org/packages/4a/24/2ecb4bde104322cd7859d6594fcfa74649f8d90b3221c9feecbef149875b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7dcd882da75ef9adf94903b1e3b9419e8aa8fb4c7396822b834b9ef7fb96954f", size = 271552, upload-time = "2026-09-30T04:37:06.295Z" }, + { url = "https://files.pythonhosted.org/packages/3f/98/9d5f6ebc3aee9fef5d30b4aff11fb2ab7a1222b4064f8ef2c7c87cde217a/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2e06a3a98f916dd41d27f3105e02e7a40181c98c94b9158733d03a6f80506c09", size = 269810, upload-time = "2026-09-30T04:37:07.905Z" }, + { url = "https://files.pythonhosted.org/packages/09/e1/a3b06a10461b1b7628853c934c644e03bc28e42767116afb52f19a56519b/charset_normalizer-3.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bd128f206a7752ae1f2ab6c61bf8a24ba28913a10df8b14c2637b973ff97a80", size = 259845, upload-time = "2026-09-30T04:37:09.554Z" }, + { url = "https://files.pythonhosted.org/packages/fd/d3/6f561f74a296cf27d61775a1dc665ad13f3bff6a798810ca05907f37a7c4/charset_normalizer-3.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c8f3d67aeaf55f017982b73683f0e7342ba2f6635a78f69ce89ebb26aa411e5c", size = 252825, upload-time = "2026-09-30T04:37:11.274Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/69e13ca3b18f43e0eafcd34c04a45b732ae22a43b54a5fc9e119103356eb/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fe9753dfee015c570d73df76f899f18444d41388bffcde097deba51c4fadbb9f", size = 252659, upload-time = "2026-09-30T04:37:12.941Z" }, + { url = "https://files.pythonhosted.org/packages/73/a9/ace29806a0dae18939919c76ba526472d83214afa101105fabff2cf30625/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:92888bb3187c5ba50500b00b3b310c9f2c651709d28036077680cb5255450a03", size = 238819, upload-time = "2026-09-30T04:37:14.659Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c1/6116d52a2e3311ec80f21f5fb5e17b27405f10b9608af8f6e69516841a1b/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d008d90a7f2471519aef0c90dfbe73b3e6e4d5e66ac48e19154c17e89e98b604", size = 272973, upload-time = "2026-09-30T04:37:16.346Z" }, + { url = "https://files.pythonhosted.org/packages/19/aa/9955c7e93bba10a9c7e8f7a5031b7ced66f3a1883a55c00712b8d5850ff3/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:31f3930700408d211f13378ccbe1c40845d8da54bd0681fac3a9b5aae81c7aa8", size = 253692, upload-time = "2026-09-30T04:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/bb/33/2a6ae7fdc1b10cb581cef91addd8cdfc5f40d50abb5702309369d5834579/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:2a925889534b3748302dae5dead07cc13480de1dac3aea80a941b729b471ef93", size = 268757, upload-time = "2026-09-30T04:37:19.877Z" }, + { url = "https://files.pythonhosted.org/packages/a2/22/80992720a0282cd39bba1db35868e6b9c22f41281160143a836544bc1d8a/charset_normalizer-3.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f5ec61164adcec446f8969a3358ec3f9b26bbda3b9213e5586d219afa8df2915", size = 262220, upload-time = "2026-09-30T04:37:21.583Z" }, + { url = "https://files.pythonhosted.org/packages/92/9f/181fd07e1bffea1d95cd80c84ac537354f50699c22cfc4d3c02b6fc16208/charset_normalizer-3.5.2-cp314-cp314t-win32.whl", hash = "sha256:598a11a2c7ebaa5334bf698bf29568c9c390abac6a154d8170fedecd1cea38c5", size = 197797, upload-time = "2026-09-30T04:37:23.235Z" }, + { url = "https://files.pythonhosted.org/packages/49/1c/25d8415ec1c4f2f41f1680435e4c87cfb378ff2f677d950946f2a45d0632/charset_normalizer-3.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:7fdde2c9fd9e3eca40631e024664cf2584272cc8f96308cbe5fdfc930f51d8bc", size = 222765, upload-time = "2026-09-30T04:37:24.891Z" }, + { url = "https://files.pythonhosted.org/packages/3e/b4/46b48f013dadfc0d0d33b375438e31bdf5a989dc68389c6bf627054d4df9/charset_normalizer-3.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d1befeed746d247c81127bb14de9dc3d30edb6e5976d34f83f86ed262b1d9105", size = 210985, upload-time = "2026-09-30T04:37:26.634Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e9/34e597dee616d0b8ee4b34d29399e85c2204ade174157a48505d42baa4ff/charset_normalizer-3.5.2-cp315-cp315-macosx_10_15_universal2.whl", hash = "sha256:87475fabc8d9996fd9c27debb395e642e8c838d78a00b6e932227a0e06b81e26", size = 367676, upload-time = "2026-09-30T04:37:28.329Z" }, + { url = "https://files.pythonhosted.org/packages/60/9f/a5d1c91c0263745e2cd344c5a4415d787c575501ab1d449f1148ac6b495d/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9409a8bf35cf78353942504b24a57de3d75b708997a1e4bd8db71ac8633ce364", size = 248414, upload-time = "2026-09-30T04:37:30.167Z" }, + { url = "https://files.pythonhosted.org/packages/26/79/e697f77464748a3ee3cf490c83d592459400d4898380d66c38366b03080c/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:498dc3188ca05a68231ac3fdbfc7f57eb67e1343c30e0fea17f8218c1599b253", size = 244775, upload-time = "2026-09-30T04:37:31.964Z" }, + { url = "https://files.pythonhosted.org/packages/ca/87/3d42a42e18ea066e2513936fd678a00696e77878b5ae04528976abdbcb83/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e242bb1c5e76e97dfa9e7f209a71e93a01d7f19ffdd5cfbb2e2d55b4f08f8ab0", size = 273877, upload-time = "2026-09-30T04:37:33.661Z" }, + { url = "https://files.pythonhosted.org/packages/c3/76/8a28136f3938ba9836f84280ce0c4d61ed1cf15a036b2034900c62634162/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def79fa35ef0cef8d2accec024f4fdc7ead3012ff02f5215c783f39f03ef8cfc", size = 269455, upload-time = "2026-09-30T04:37:35.573Z" }, + { url = "https://files.pythonhosted.org/packages/a0/a1/4fbf5d0f0f1b2a080474c1cf9a2f12c4c6531bb0e8ba591055e846d2b4e9/charset_normalizer-3.5.2-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df041de8887954562c9b261cba85ca0e9ded74048daf125f45edcfaa4832229", size = 257336, upload-time = "2026-09-30T04:37:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/ba/a2/8b50aa320adb880ad579518e6f718f24944804b42a88b83d267d5d444125/charset_normalizer-3.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:04851f73ae72b8413dddadb16a49dfee95263553741fd42d546f7d66907e6be5", size = 257584, upload-time = "2026-09-30T04:37:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/50e3fed84e175f40349bd0da7a4fce94c87f0378f52d74f511d89e0bdc20/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:183b88127acdb4fabe59d951ab424faf1af7b63cdbb5f776186c1ea2ffcaed98", size = 251854, upload-time = "2026-09-30T04:37:41.23Z" }, + { url = "https://files.pythonhosted.org/packages/d6/54/f7fbb3493c9f49091213b9c2d6dd65800696f1ce1a3f196a4205f50417b1/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:16fa0eccf81304b79c5cd87f9271c3b85dd9dd99245e4422ae9c0dd45e0f99d3", size = 248767, upload-time = "2026-09-30T04:37:42.883Z" }, + { url = "https://files.pythonhosted.org/packages/d9/37/b3a6385acc5a1e45b39ae9c90bfb9cf838a09b9dd37ef2740ab4c6b4a2eb/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:7441d755b7ab94f8d4eb3e43ec05482d760842fd263d003a99102d742cd835e2", size = 274479, upload-time = "2026-09-30T04:37:44.658Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/809913e2cfd279e635a9294fdbbfb1b1dc62a8189d473d561f649fce98d8/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:ca403d7e4798f525fdfc78e258820419cbbd0f0ecbab9de7840e3c017cf6b8cf", size = 258310, upload-time = "2026-09-30T04:37:46.529Z" }, + { url = "https://files.pythonhosted.org/packages/af/a2/f28400ab13359d91bd39179df8e149376b9bf36588e739a3a4f9de2b84b2/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_s390x.whl", hash = "sha256:df29a0a7107f7011e77f4eebdddec4c7331e24d787a0b21a46d63bdf7445da95", size = 271100, upload-time = "2026-09-30T04:37:48.399Z" }, + { url = "https://files.pythonhosted.org/packages/e9/89/9bab37955edf0adb3b66f8a3a6617d9f2f487e0d56f295a6a286cb640aa6/charset_normalizer-3.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:f3c96f633825733f735c5a9cf21d21a257d8e1edf0b1cee0a064b9c424ca0f7d", size = 259936, upload-time = "2026-09-30T04:37:50.023Z" }, + { url = "https://files.pythonhosted.org/packages/23/b5/4459e08d45a679f903d50fea08bc52cfa728cca4d7bd02c757b5e5abda2e/charset_normalizer-3.5.2-cp315-cp315-win32.whl", hash = "sha256:281cb91036248400f4cc957495cccd44c275c2e0c5854f7e45ac5cf7dc193847", size = 186301, upload-time = "2026-09-30T04:37:51.722Z" }, + { url = "https://files.pythonhosted.org/packages/98/e8/55d5fd3935b4bce6da4fe0df61898e8c82653e317e677bd58aceb9c60f13/charset_normalizer-3.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:89b53f3cda69831909888e0494f4fa0bcd3537e3e138dabeb620bd6ad946bae8", size = 210858, upload-time = "2026-09-30T04:37:53.427Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/974423c2fd8e524c7a7f64318c1e02240ef954912fa2b4d70344107b9c68/charset_normalizer-3.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:6be488a102b8cf28d0391d8c4ba7748938ae28b78ad901f8585520fca33ead1a", size = 200309, upload-time = "2026-09-30T04:37:55.015Z" }, + { url = "https://files.pythonhosted.org/packages/ee/f9/00ee0195db1013d8f7c416fd770fbeb560bb46eb2e36b054d05cb56f6cfa/charset_normalizer-3.5.2-cp315-cp315t-macosx_10_15_universal2.whl", hash = "sha256:915563965d418f986e7e145accc592eae9e1a1be3566ff98a05d7a9ec42a76e1", size = 395343, upload-time = "2026-09-30T04:37:56.743Z" }, + { url = "https://files.pythonhosted.org/packages/04/3a/c00b50e94c964cf934c7899cd47c97952fc11dad71cc5884b3c61795b09b/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:65cd72beeeca9d3aaea1201e5923859f308f952f9c71de93f06063c79f0f7a3b", size = 250114, upload-time = "2026-09-30T04:37:58.607Z" }, + { url = "https://files.pythonhosted.org/packages/50/27/d102dc880bbcffd0479ab64dfc1fb96777a854355a55e2bda72a71efadcb/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b7fd005a73d9e657273b7a10dc71a9e03c8fb9ee6999798d6918ce095b81ac7f", size = 239997, upload-time = "2026-09-30T04:38:00.511Z" }, + { url = "https://files.pythonhosted.org/packages/a5/4a/bf7ef45794dd293fab5f98a9309817977fbb845b9998f171b8cc5d8437a3/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e54da4baf05720032d527874d40b65fa4d7e5c6c6a43d0c3adbeffcaf275a2b3", size = 272313, upload-time = "2026-09-30T04:38:02.509Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/008a2837737991474c5754bb3191010007663860979701990982a502cbaf/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:124fbf1a8ff966d87ae05bb8bd45a71f966055ed8bba320d0c7cf450bc5f4d0e", size = 269986, upload-time = "2026-09-30T04:38:04.435Z" }, + { url = "https://files.pythonhosted.org/packages/93/ad/bd74a283940dc910c5b14f8e4f80a248082bc9c0fcbe1f54530cb6d9cc5e/charset_normalizer-3.5.2-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28b4f0d66fb834ff90f28209ac7bce77868c45d8c93e26f906709d9b7c2e1af9", size = 260202, upload-time = "2026-09-30T04:38:06.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7b/ed341c66f69f688723501fac752be3d63c7159ca0d0d4174fc611e5710bb/charset_normalizer-3.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:58ca3755ee7ff7f59b57789ec9833c9de9ea275405cdd240eda1f193112e398a", size = 259083, upload-time = "2026-09-30T04:38:08.311Z" }, + { url = "https://files.pythonhosted.org/packages/cc/9d/e41588b777965e5031a43128a1e96173ebb35ac75fc53ec3b517e7c21cd4/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:443eae2bf318abeaf6f15d785138f71fd6de770e99a92158b8b814265e079115", size = 253081, upload-time = "2026-09-30T04:38:10.402Z" }, + { url = "https://files.pythonhosted.org/packages/81/35/b761eb6d8c1eb218b9b42b9b4d5ac902afdc399fb6dac6f9a9aac7bda589/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:58f361dcbab699cf8f42db3f47c8e7fd1036f138c23a5d08de9fde5f425a730c", size = 248712, upload-time = "2026-09-30T04:38:12.317Z" }, + { url = "https://files.pythonhosted.org/packages/4d/2c/147169a041b747759f37405c0a97157e8e92de967968373101ff14915cba/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:1b4cbc7c3491ccb4aa17fcd8165649d01cf39f76de1696da8631b5f71b85401d", size = 274176, upload-time = "2026-09-30T04:38:14.138Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2d/0ff8db0d373ba8538db686db11cd7e8912031490b9e4f383b41912e8d594/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:ba0b1d2620edf869789c3879223f52bf2afc5d31b3cb47cc57b3a12c05e2aa9d", size = 259413, upload-time = "2026-09-30T04:38:15.841Z" }, + { url = "https://files.pythonhosted.org/packages/8a/8e/b4a085fb47c9d3a7e43576a4784fdd8fe23f907514a972de8086edaf7a48/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_s390x.whl", hash = "sha256:5e2b6b57e9733d39f0c9fd3185efa6b8e29652c4cd8fe94180272cf6ed9a78c4", size = 269434, upload-time = "2026-09-30T04:38:17.626Z" }, + { url = "https://files.pythonhosted.org/packages/83/1c/d8d8d7322a7c3eecdf3237a4a419cf41d2eaad8e006ce7dfdd9d4c8fa2eb/charset_normalizer-3.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:51cf45226a9b588d0d2b4880c62d686934b63ab0bd79ca23ab0e9762eb27441b", size = 262716, upload-time = "2026-09-30T04:38:19.214Z" }, + { url = "https://files.pythonhosted.org/packages/a0/16/0e4c6ba9b44e97a2da150e52d331e8f9c968b21b358fbffa6c856cebcd89/charset_normalizer-3.5.2-cp315-cp315t-win32.whl", hash = "sha256:5fb29fb8cd1a46c27a1bf9613ad5ec2599310d46b4025d9556404a6b6a292800", size = 197159, upload-time = "2026-09-30T04:38:21.037Z" }, + { url = "https://files.pythonhosted.org/packages/be/33/e90bc2b1374f7f36ef106f56620de5a783907e19ca857efe2277e31cac3e/charset_normalizer-3.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:a192e2c40070d92c3ccf777e3a5c4ff515573cd2bb7ed0c537fdadbbec5bbf21", size = 221322, upload-time = "2026-09-30T04:38:22.886Z" }, + { url = "https://files.pythonhosted.org/packages/66/89/dfa6dcb08c200b7830ab56439e8c1890f2971d51aafbb3937894a2e7fcfc/charset_normalizer-3.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:749e97e1b32313717a565abbe321bc2190bc8b35f1a67e4cdbc7c56c8d8ffe58", size = 209192, upload-time = "2026-09-30T04:38:24.648Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ab/176fbfd5b64939c55d652366aa5b9ef1d767af207a3aa6ebeb0d226c484d/charset_normalizer-3.5.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:4275811936e2f06feff5e598fb42a1b7ae852da8e39605211892b56b81a34efd", size = 331815, upload-time = "2026-09-30T04:38:26.216Z" }, + { url = "https://files.pythonhosted.org/packages/7e/84/371eac6b30bdbcbf2d632a1a01809103459216fcaae61b8b8d922c1bfb8a/charset_normalizer-3.5.2-cp37-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:1c50fe28bbc2ced33386f298650d91218076c05420e6cbd790b913adc41659e7", size = 253276, upload-time = "2026-09-30T04:38:28.032Z" }, + { url = "https://files.pythonhosted.org/packages/43/6f/c4fbae58febff71709c51bc7e18fdfa55341dc382704740f9f0cbf03817b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d19fbd981a488e22cd04883659ca6b08f50b5974f9fd7c95655ef6a043e5893f", size = 241239, upload-time = "2026-09-30T04:38:29.732Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/458c3f42164a07d0c5210798e9e704b39e540a6793b05aba67f3a35243a9/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0fed1d06615f022ee3b13caf5e8b180cfea32bb2c5aded8a9d44277afc040f93", size = 231121, upload-time = "2026-09-30T04:38:31.462Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/ab9e89367076f6331bb6c65c4bf14a5361fa5191cb6561bf534f18504e1b/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:838dcc90063569a0448120554591a1d6c4a4ffe11babf048908793154ab86ade", size = 260350, upload-time = "2026-09-30T04:38:33.239Z" }, + { url = "https://files.pythonhosted.org/packages/7c/c1/061431ecc688d9d76602502cb57cc01e691e682c18f1beb45f9673b5bbd2/charset_normalizer-3.5.2-cp37-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2ce45c6627b22c47e390bc91a41c3d13032192e699fa0bea96e9671b373d69b0", size = 255430, upload-time = "2026-09-30T04:38:34.865Z" }, + { url = "https://files.pythonhosted.org/packages/8d/1f/20c8949f0676f7ab811abdeb7f4d7f1cbc6e61ff20bef08b44edeb092bc8/charset_normalizer-3.5.2-cp37-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0774bf9bf620249fee3e0b8b9fd3065de213be30f3aa94ce2494b3b638949e26", size = 250612, upload-time = "2026-09-30T04:38:36.649Z" }, + { url = "https://files.pythonhosted.org/packages/2b/9e/46f2fa4c431fc98c4ae76a8cb5bdca54e0341e3cfc3fcfd8e82740250818/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1db38f4c5496827c1a501846d64d14c3b80c7e6714e406cd7dc36a9899fa1011", size = 242083, upload-time = "2026-09-30T04:38:38.26Z" }, + { url = "https://files.pythonhosted.org/packages/bd/39/559be29a0c0f086e0bba6922babd38916cc5e0b58ced4de13ee01ea05508/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:304d8e4d493af723536393eee0c689eb7813f4a474c8b479dee63f1fdd98f621", size = 232738, upload-time = "2026-09-30T04:38:39.81Z" }, + { url = "https://files.pythonhosted.org/packages/ff/6c/387b0e4f756a282831c1d9fc6aeb6c51ca4507ca202767c8de15ce9b12e2/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:9b7f416ff0978e2f2249330527f0ad6fa02f4932e6199692d3b52da2048c19e4", size = 260703, upload-time = "2026-09-30T04:38:41.346Z" }, + { url = "https://files.pythonhosted.org/packages/96/92/1fdf015f09ef449f50d3ac4b67c90887c9c318b727daa95cc4f866e6521d/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:01077390b03f7988f11d700a2194e69b119741a86b1a638b1db88891e3eced8e", size = 247622, upload-time = "2026-09-30T04:38:42.937Z" }, + { url = "https://files.pythonhosted.org/packages/dc/3c/8e7b8a5671ad5d433669fb2a76f1a0164df2d9b1718b0206bc2a16d840cc/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_s390x.whl", hash = "sha256:7e841fb9010836c992c9f12fcbd43a831de93a5f726fc1ccd8ca1d0268c5014c", size = 257500, upload-time = "2026-09-30T04:38:44.604Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f0/45b579df5cabc1d5d53ea1cc35e8437d3ca768c0acccc7041517cb6fbb32/charset_normalizer-3.5.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:9cae88599c7219005d879f98e5ed53341e9a122af585e1091200358a3003d2a0", size = 255100, upload-time = "2026-09-30T04:38:46.289Z" }, + { url = "https://files.pythonhosted.org/packages/31/68/fdec18a343f5fb3f310588dd478b09ac4799e0b187dbade3a8cd776f03ef/charset_normalizer-3.5.2-cp37-abi3-win32.whl", hash = "sha256:01b0c0d2262a9e28e8484a278c7e1b5d650e3ac8cf2683d2967e25899f208bdf", size = 174499, upload-time = "2026-09-30T04:38:47.999Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8a/b618149cc5207943a0242068d7a27897f56a62947b5a039085f2a22029f8/charset_normalizer-3.5.2-cp37-abi3-win_amd64.whl", hash = "sha256:9f56f72050826f63dcee7a7f55b0a77168cb3bfc553fd405e7f8f9ece75a4036", size = 200092, upload-time = "2026-09-30T04:38:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/03/cf/4c66866fa9e2b1c78e3c911516d1de497a677b7ac60f1eceda74ce777ca3/charset_normalizer-3.5.2-cp37-abi3-win_arm64.whl", hash = "sha256:40ab6bffa02ae10a0581e6c198be7d2d8ca5c2a0c64e4ed3465d766df457573e", size = 294363, upload-time = "2026-09-30T04:38:51.312Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ad/d07d7862a62ffa6d79d68074d14823243dd235a77c45262acbf6adeb28bf/charset_normalizer-3.5.2-py3-none-any.whl", hash = "sha256:b6b751274acb69d77b3323d6b7dbaa3c7fdfc1eb829b7eb61d262f32e1af9685", size = 68872, upload-time = "2026-09-30T04:39:21.828Z" }, +] + +[[package]] +name = "colorama" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, +] + +[[package]] +name = "commitizen" +version = "4.19.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argcomplete" }, + { name = "charset-normalizer" }, + { name = "colorama" }, + { name = "decli" }, + { name = "deprecated" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "prompt-toolkit" }, + { name = "pyyaml" }, + { name = "questionary" }, + { name = "termcolor" }, + { name = "tomlkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/bc/294ceb7ba99f83dca2bf62942dd325ffca86e504aa41cc42c2982350bd2b/commitizen-4.19.3.tar.gz", hash = "sha256:db77b1d521b5b9a16231a4fd5093d09ca519c5f170938806a784309b81938419", size = 73784, upload-time = "2026-10-24T07:24:08.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ae/4f63a84dd2db955a3b222b2df002ebff61387fc80efdf46f611f9f4dd308/commitizen-4.19.3-py3-none-any.whl", hash = "sha256:d625e038cce66d92da20d24c2c366d410caca43966eba8ec036debf48fda2a68", size = 92037, upload-time = "2026-10-24T07:24:06.801Z" }, +] + +[[package]] +name = "decli" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/59/d4ffff1dee2c8f6f2dd8f87010962e60f7b7847504d765c91ede5a466730/decli-0.6.3.tar.gz", hash = "sha256:87f9d39361adf7f16b9ca6e3b614badf7519da13092f2db3c80ca223c53c7656", size = 7564, upload-time = "2025-06-01T15:23:41.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/fa/ec878c28bc7f65b77e7e17af3522c9948a9711b9fa7fc4c5e3140a7e3578/decli-0.6.3-py3-none-any.whl", hash = "sha256:5152347c7bb8e3114ad65db719e5709b28d7f7f45bdb709f70167925e55640f3", size = 7989, upload-time = "2025-06-01T15:23:40.228Z" }, +] + +[[package]] +name = "deprecated" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wrapt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/85/12f0a49a7c4ffb70572b6c2ef13c90c88fd190debda93b23f026b25f9634/deprecated-1.3.1.tar.gz", hash = "sha256:b1b50e0ff0c1fddaa5708a2c6b0a6588bb09b892825ab2b214ac9ea9d92a5223", size = 2932523, upload-time = "2025-10-30T08:19:02.757Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/d0/205d54408c08b13550c733c4b85429e7ead111c7f0014309637425520a9a/deprecated-1.3.1-py2.py3-none-any.whl", hash = "sha256:597bfef186b6f60181535a29fbe44865ce137a5079f295b479886c82729d5f3f", size = 11298, upload-time = "2025-10-30T08:19:00.758Z" }, +] + +[[package]] +name = "distlib" +version = "0.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/68/bc/d36966de7304047a1001a7559fbf266c464bb5d8cc85464536ef8c9bcc32/distlib-0.4.4.tar.gz", hash = "sha256:ca4175b4ac7f71223cc30ed7248c45d2b38c2439d8ee6e498ab67ed63cf3508d", size = 621304, upload-time = "2026-10-16T13:56:34.203Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/2b/238f02a4927fa2e27aa995371d240a468b52e42eae44a678434aff736b38/distlib-0.4.4-py2.py3-none-any.whl", hash = "sha256:cf09d55b1e85a89576b036aeb71550bfd68320f3f21761ca329eadadad918638", size = 472099, upload-time = "2026-10-16T13:56:32.053Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, +] + +[[package]] +name = "fensu" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fensu-cli" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/17/69/a132ffc407be97a0fdcfa052cb8b49408091db748e6afe0f7a6c77e6fa7d/fensu-0.28.0.tar.gz", hash = "sha256:11bcea3207f7fd92babc200f40d074e3da789ee11e7fc7024575a415de7c1176", size = 538990, upload-time = "2026-10-27T00:39:16.19Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/59/e9/7434bd0565d54ff58796105728b3ccc8b838c7200997184978e6ebfdec4d/fensu-0.28.0-cp312-abi3-macosx_10_12_x86_64.whl", hash = "sha256:29df31aff900d20b0c845940af0cd91eead55e158a2511e682e3e887a22dada8", size = 4154207, upload-time = "2026-10-27T00:38:52.311Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d8/e0875b269515d9a75643d5488bdc6f70a4b6c346995f12ce07304a3610f7/fensu-0.28.0-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:33a6d8b7d2f734c48f48087462c0e5a1ff94535c0e27e6a4820bc10303ff3653", size = 4004243, upload-time = "2026-10-27T00:38:54.994Z" }, + { url = "https://files.pythonhosted.org/packages/e8/05/b87c0ca5cf3f724517d6958f1ebd68948f9399306bd6d6814783635bad9a/fensu-0.28.0-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:de07b3fee5d908d2a766ca136753c8040d6fe25a1346d9b3b5a98483fc23d874", size = 4230192, upload-time = "2026-10-27T00:38:57.321Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d2/fdfc7fd322c3497c53c22754a33433e33678a6de84832fd41add09481a21/fensu-0.28.0-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f44293a2da6bf4558d00eb4f859b28f3fb69d396dad1c25ac466a92a7d1dd734", size = 4307267, upload-time = "2026-10-27T00:38:59.679Z" }, + { url = "https://files.pythonhosted.org/packages/56/c0/2ab2c2c1af4ddd1b45c750679bd9f0b777d7eca3c0e00a233c991aaa13e7/fensu-0.28.0-cp312-abi3-win_amd64.whl", hash = "sha256:ffa88f1d259a1830d513ae930ff856d5b5e4941ba433889dea04d3db5753f281", size = 3977828, upload-time = "2026-10-27T00:39:01.761Z" }, +] + +[[package]] +name = "fensu-cli" +version = "0.28.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/d9/f347f514fb224f6a1f4ca6e616ac49b573f9dceba4ff8d9efcd334f48bea/fensu_cli-0.28.0.tar.gz", hash = "sha256:1feb379ed92e193cbd4d28ab3d7729798b88120aaee2f59fd7f341519b6ee945", size = 765638, upload-time = "2026-10-27T00:39:17.752Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/b3/57f1de89e1cb7534b0ef6415822c8982a247b9d2efe8d2fddd85e35389c8/fensu_cli-0.28.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:13ecc7a224a4b32eef19456f21659e682e636ef53f305919aa57cae27c8e3fec", size = 7978247, upload-time = "2026-10-27T00:39:04.644Z" }, + { url = "https://files.pythonhosted.org/packages/ff/ff/2e887d727f82c61ea7cb6773429b61d4711b93f7f94814eb9a7e891384dc/fensu_cli-0.28.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:149e5e22c8895c0a3ed3956b30e693e78df7dfb67ce8e0c5682b84c77e811e7e", size = 7490546, upload-time = "2026-10-27T00:39:07.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/4e/daa4c2d70e5b0398382f84c846aacc4a937c4d00b3938d00a67351b0f212/fensu_cli-0.28.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:99932d17956bb10da1ac677867567b504a961d0565fcda730c1e682434244113", size = 7570220, upload-time = "2026-10-27T00:39:09.852Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f9/c9e1095c507da3a53b3e7bf3f7c95f75824429ea45a72cb2c7af67a605f7/fensu_cli-0.28.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee51c5d6d0d5fa0b36c0f70d3d6fde429793b643f0075cdfdf3fc8b8d73e3ccb", size = 8086364, upload-time = "2026-10-27T00:39:12.052Z" }, + { url = "https://files.pythonhosted.org/packages/9c/89/ece32e452d2b4ade243dd66a2612619be7400f6acf59f854ce674bb78e30/fensu_cli-0.28.0-py3-none-win_amd64.whl", hash = "sha256:acca6ac39cbe0614d3a8e4ce7c2c12cc5fd3c800fc4c66b5484c54dc19ef4a66", size = 7700477, upload-time = "2026-10-27T00:39:14.15Z" }, +] + +[[package]] +name = "filelock" +version = "4.2.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d8/28/3893324bcadf8f89b1616637468d766af3f625008c0735bf704fdfa86617/filelock-4.2.4.tar.gz", hash = "sha256:c6d8663535e1650ee430507b4ffbac7b464dd64b7647ea0112a9f406499d163d", size = 652444, upload-time = "2026-10-19T01:57:51.001Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/1b/5e74289a1878e666bb4b65fe5805a04cafcac2aa5f519ef1ead67ef91c56/filelock-4.2.4-py3-none-any.whl", hash = "sha256:301241438c57936ef93f4243a94f2559022268bfcf9b375a9a72eb0f8db8358e", size = 162475, upload-time = "2026-10-19T01:57:49.419Z" }, +] + +[[package]] +name = "identify" +version = "2.6.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/35/d70c0006c7cee65999ea94a6273e60b2094f600a3d8b71b04318253fc643/identify-2.6.20.tar.gz", hash = "sha256:ad729860a923858d26917c2f4fb0a1d83d27a75b1e090c06440c573f048f3285", size = 99681, upload-time = "2026-09-26T20:29:07.187Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/70/fffc9613501877c0a10a1ef73a165e6ded3e53f6e7502227fef56d973933/identify-2.6.20-py2.py3-none-any.whl", hash = "sha256:6a16b69b93187244e0548cbfd25b3e4a6f9a7a2ad784625c3bec2b8d27b81aaa", size = 99506, upload-time = "2026-09-26T20:29:06.054Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/e1/2069291243c926a2ff1cd706c7f3eeb9b62144bf60f77c9fb9ff2fb26bd3/iniconfig-2.3.1.tar.gz", hash = "sha256:67f4b9c50da0dedf52af349e7749a80a9057a5031199791b906c3bb3ae878960", size = 21209, upload-time = "2026-10-06T22:48:38.076Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/43/4ca9e49d27a1fcf6bece6f6aec0ea46bb9112489b93d4b688fb415457bdb/iniconfig-2.3.1-py3-none-any.whl", hash = "sha256:9121e2c1fdb355232495be3194c8dfe87ccc2d5dee45947b78e68f499790d7a7", size = 7552, upload-time = "2026-10-06T22:48:36.959Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/9b/e422a865e1d5d57d0e509b4e0bf1c1a70a7f6382c29a5aa428df994c8bc8/markupsafe-3.0.4.tar.gz", hash = "sha256:2e9ad7dd851bf45fab9f75cbff4cb493fee9979e8d8c7c9c3ee119022518edd6", size = 153777, upload-time = "2026-10-02T23:07:22.29Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/09/4c59d56b8461ae8eb0d8ba34bb25b7e618547044679d58a82ef9b2479fc1/markupsafe-3.0.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:61631e08084be9e21a8967ec3139c7616ed7c5e9368e05c86d1b39562c8a57b6", size = 11658, upload-time = "2026-10-02T23:04:51.876Z" }, + { url = "https://files.pythonhosted.org/packages/a2/f0/d6613774d86fbf6d145751d43c59875e47a6f9f17daee0aef173bd36d90e/markupsafe-3.0.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0930db9bdc62d22944e10b066448bb65dc9abe9112880c7cab8da54db4284d5f", size = 12050, upload-time = "2026-10-02T23:04:52.931Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f2/8f18e0b806eb13c1f8d07d917a720831ead54253a6dec011fbc78098a6f8/markupsafe-3.0.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a45c3d514f2436064db00d7fc8778d888f0236ebfed649b53d13a59e69ad51b", size = 24363, upload-time = "2026-10-02T23:04:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/60/ce/fa07dbe8a5675558fa36dea033e19995bc783de2dec5f540ccb9030b06aa/markupsafe-3.0.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:1e1451fab512d1bcc3dc26988ec1edb0b82c2db909132872cd9356070a6b63df", size = 28525, upload-time = "2026-10-02T23:04:54.905Z" }, + { url = "https://files.pythonhosted.org/packages/85/40/be87c01f3868ec217f8a2015089d71c22c8c5a75324822e5ed1cdd87210d/markupsafe-3.0.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bd3ce56ae2cbae3ba82b683bc425cd7e48d2ed8b10f3e818186b6f5646d9271c", size = 24733, upload-time = "2026-10-02T23:04:56.229Z" }, + { url = "https://files.pythonhosted.org/packages/4f/a7/aeedb5140afa41fc74c225e9184ab96723a6e873b6ee1c9fede7283456d8/markupsafe-3.0.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e124f974786f831d6043728e38296969d3579db8896fe004682f5758e613581", size = 22985, upload-time = "2026-10-02T23:04:57.521Z" }, + { url = "https://files.pythonhosted.org/packages/c3/fc/e91352bb08c6a59da3ef0909d457bf95a5f5908fbf151b30a06d9dbcfbb4/markupsafe-3.0.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c02e8f18bdedba082cef725942ac823b9b60656db07f7e265cb31618dfd00d77", size = 22001, upload-time = "2026-10-02T23:04:58.597Z" }, + { url = "https://files.pythonhosted.org/packages/5d/f8/bffee5e7d2a3deb59748a797650a48af7e672025cf641a79344a771ad106/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f098115c247e11d138ab83a28fa0323c77015007ea2df73ba5fd714dfefd67c", size = 23793, upload-time = "2026-10-02T23:04:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/ed/59/b853d6628ecb4d658e1d637224846d5e9bb4adf4f8df97f3be9f29dce2ec/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:d5f93ebbeb8032d47e349328ec8662d973d9b05a70b3c35df1f91fe419b84749", size = 22640, upload-time = "2026-10-02T23:05:00.768Z" }, + { url = "https://files.pythonhosted.org/packages/09/b2/1506df394f0f075797c418d0301498f49e43be194e3ffcb49e6fe6ccf022/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:64511c54db4e4987aef4c41923235927428729e8174c5dba488429be70a998ed", size = 24074, upload-time = "2026-10-02T23:05:01.813Z" }, + { url = "https://files.pythonhosted.org/packages/c7/81/5ed69cda630ac69ef60d06c09ba5a7f84ff66a2e28cf986fd5614ab3c6e6/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e1a622f13970d81f95d0c72f9dc090dce9085fccfa4c9f2174377ee32bd15786", size = 21563, upload-time = "2026-10-02T23:05:03.239Z" }, + { url = "https://files.pythonhosted.org/packages/0c/fe/fb1e79be0fea60aa32602ebefc9c35a82bb42b4df157285ab7dfec12341a/markupsafe-3.0.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c9a7f43c0b202b334cc9184af09bb8f21d3a209e038efaf106936fb69e6b026e", size = 23048, upload-time = "2026-10-02T23:05:04.479Z" }, + { url = "https://files.pythonhosted.org/packages/c8/52/7632a53360671a9b750cdbabaf9cdd89f18b42248b8e4cb42c0b0296e459/markupsafe-3.0.4-cp312-cp312-win32.whl", hash = "sha256:f0ec3b750b59375eab5b0fb2b9254810c00a3375be6d789899f1055a1d556237", size = 14108, upload-time = "2026-10-02T23:05:05.513Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/62495e180b7000aaf30000fff849e933f74264638057176cf46852500adc/markupsafe-3.0.4-cp312-cp312-win_amd64.whl", hash = "sha256:11935df9bf455ed0c04eb87bcd720f02b1fe5e02128a9430f23aed6f93336fc7", size = 14303, upload-time = "2026-10-02T23:05:06.538Z" }, + { url = "https://files.pythonhosted.org/packages/c5/8e/4c24208776a65878d656996945aacfbfe010d3720d1a98fc0eb8491fc03b/markupsafe-3.0.4-cp312-cp312-win_arm64.whl", hash = "sha256:a4bbd2d87dd233b9fc5812160c3d0ffbe42edc22a26ce0469f58479ede633fe9", size = 14174, upload-time = "2026-10-02T23:05:07.617Z" }, + { url = "https://files.pythonhosted.org/packages/6d/18/4bc5ba32499e87bb2b0ef5b3a9bb9c00a131fa961ddf0be548cb550f548b/markupsafe-3.0.4-cp313-cp313-android_24_arm64_v8a.whl", hash = "sha256:de8b364c423ef0a4bad9069657d617f9a5d2b2062457a89b1fa16ee199c399c1", size = 12786, upload-time = "2026-10-02T23:05:08.709Z" }, + { url = "https://files.pythonhosted.org/packages/4e/6f/17f0c099bf25f3e31e63cc19244d9f6af861a9a4ab778c203997903cfdd0/markupsafe-3.0.4-cp313-cp313-android_24_x86_64.whl", hash = "sha256:34bdde374c5932765d7dc685c4a1d191a3207852d67e8e0a9eb6ea85156181f1", size = 12510, upload-time = "2026-10-02T23:05:09.93Z" }, + { url = "https://files.pythonhosted.org/packages/11/af/1a141081b905036ee904ec4bd945e1f70b4e1b32d33c4e59e8cf1d58b247/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:6bd9e1788e15bfcf6a9082de42e30387e7b85d211ab21e57a939bb8cfaaf8d96", size = 11583, upload-time = "2026-10-02T23:05:10.884Z" }, + { url = "https://files.pythonhosted.org/packages/e7/0a/a89385ae590232622a03e091805cff12f24fabe6c11e0e8bae096cece81c/markupsafe-3.0.4-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:5066b244f576f91afc8ee3ba029a89f99d39c79b1853fe9d39bea9f0afbec148", size = 11801, upload-time = "2026-10-02T23:05:11.913Z" }, + { url = "https://files.pythonhosted.org/packages/ed/85/ea548dc013962eb73653124bc595635fbf9e0fa41d1f181a967ccb784dfb/markupsafe-3.0.4-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:7a83aa6e4805df46fed18e989d3d16f86ef60cb50bbc8d9ce3a6be89165fbf6e", size = 11443, upload-time = "2026-10-02T23:05:12.887Z" }, + { url = "https://files.pythonhosted.org/packages/cc/72/15f2e5ec9cf2eb00d5cdfe968d94e4156a7bd7303832c3f3b2c403a36839/markupsafe-3.0.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2d1b7d9308288661f56672b1b157d75fc536714d3638487bbea17b6318a78248", size = 11665, upload-time = "2026-10-02T23:05:13.829Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e0/4030bea613677e333c8a2c901fd405055f657f9d06acba5b7357984b6ef7/markupsafe-3.0.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:73e77980c7207854f00fc4e71fb1626868d5740ab4012623d55c7a99ad122a72", size = 12061, upload-time = "2026-10-02T23:05:14.807Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a5/28b76a7449eb702966b88bef599e2360b411fbb3afeee8fe560939be06ec/markupsafe-3.0.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7018d4af1cd272e847aa5917983ab5e83e4f6579f9dbfecd4a79c0ca80b144c2", size = 24395, upload-time = "2026-10-02T23:05:15.909Z" }, + { url = "https://files.pythonhosted.org/packages/07/6c/21232811afc3a063b5e934b1ae2efda52f46154ec382f585149c020e61fe/markupsafe-3.0.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c90d5b3d4e944e065a301d741b3c1d784f6bd1f503aa68b4967e32b2ba313d85", size = 27183, upload-time = "2026-10-02T23:05:16.976Z" }, + { url = "https://files.pythonhosted.org/packages/14/38/6ccdfa5b59049cb36fb80cbc80aee9cf1fc9bb77d1335ad435f2070b08cf/markupsafe-3.0.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:18a801868a884f216e784d7d14db2a4077143ce7610440aee2ce8f734e7cfcde", size = 24772, upload-time = "2026-10-02T23:05:18.209Z" }, + { url = "https://files.pythonhosted.org/packages/63/e0/cec6865dfe88cb48fedd4b20aed6af5158e41092adcbf3e028bcc6ec2108/markupsafe-3.0.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:434139499bb20b502ed3baa1f169e618f924a97e7a777fea1a49446d80106cf6", size = 23017, upload-time = "2026-10-02T23:05:19.286Z" }, + { url = "https://files.pythonhosted.org/packages/ee/76/6ed4940bb7648a9aac457c14f870cfdd5105f139a0fb1f29cd61fafa47d1/markupsafe-3.0.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9e227f3dbe6bde7491cf0a9965d00b88c6b1a4a95d11480ddf88bb96d397c19f", size = 22036, upload-time = "2026-10-02T23:05:20.352Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/ed476226d4fe46a09090a36025bf319296810028df55eb12f1253b540f3a/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:b8cd1f918b26fd7b1832ece557cc18f2d8747309ff8b3f0ef9d4250c5ad67a39", size = 23786, upload-time = "2026-10-02T23:05:21.576Z" }, + { url = "https://files.pythonhosted.org/packages/9a/35/66ff30450e35ef5fba9ebc930c9411747e537fd9447b65e44f5007e2b84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a5fcffb37e602b0b3c1638a97746b9b96125caa9bcf6fa41d337a9261de231ee", size = 22672, upload-time = "2026-10-02T23:05:22.922Z" }, + { url = "https://files.pythonhosted.org/packages/32/0b/72f45ce4b4efcbca4b80cf1b06703eff0be8d37e82abb78f66c85a7ead1e/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5989cb26b2e1efc6a42216a9f6b5ee495ce5ace2e5b352a9af489976b32d1ee2", size = 24107, upload-time = "2026-10-02T23:05:24.175Z" }, + { url = "https://files.pythonhosted.org/packages/d2/03/71776e5fdcba04614b384cc102e8a4198208579d896fd1394cb7cb9aa900/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:add96447a86d205ab616665d53b2950ee81083757f56e6ea833c8b2917646b46", size = 21615, upload-time = "2026-10-02T23:05:25.215Z" }, + { url = "https://files.pythonhosted.org/packages/ab/5f/801ce02a02e7aee0f784b1ec7843026178f6adeb9c93ac67eb1992a9a84d/markupsafe-3.0.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2628d3a8cb648ecebb3c5d6b0a1052d400e4d8b7ac0fb786be8d285b50040d17", size = 23076, upload-time = "2026-10-02T23:05:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/c43776625428f3bb4a61e8633940400e3efe6409e3c6f5bff26de5e45618/markupsafe-3.0.4-cp313-cp313-win32.whl", hash = "sha256:672d207103e6b16ca098611b0f9efad6bc00afd47c03d6ef62186495ca677dc0", size = 14118, upload-time = "2026-10-02T23:05:27.716Z" }, + { url = "https://files.pythonhosted.org/packages/6f/36/163da64de88a13db79214ef75fa041be7fa13bdb42261cf5b7484de14bfb/markupsafe-3.0.4-cp313-cp313-win_amd64.whl", hash = "sha256:1f1f9477e174582b0a1b583d60b66e1f2cf5d3fe12cee985e4aedf44766600e5", size = 14313, upload-time = "2026-10-02T23:05:28.749Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a8/9b662783ffaa1149221432a923cee562f78b9cbbb8baa3df9b3753e63e1e/markupsafe-3.0.4-cp313-cp313-win_arm64.whl", hash = "sha256:06de8ef6331f6e822c28d577dc8bf43fe398800477c49498f38fc38b67ff33fc", size = 14189, upload-time = "2026-10-02T23:05:29.917Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c3/a944f3b0df22bd129e96915b9f4e98d2eeca6516687d7618304a966c3c74/markupsafe-3.0.4-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:4ed644d75aa94a2baf7ec3a96eaa160ea58c742eb9d27c6506053c5c40fc84ed", size = 12787, upload-time = "2026-10-02T23:05:30.971Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d6/a44863f69d88b6c7e27889108f70d47aed259edf89d5df3c5fca1eac87d6/markupsafe-3.0.4-cp314-cp314-android_24_x86_64.whl", hash = "sha256:6d2a9efe686f9de00d0d1ea32a4a5a86d558a2277501bd78d964214eab625e59", size = 12525, upload-time = "2026-10-02T23:05:32.263Z" }, + { url = "https://files.pythonhosted.org/packages/17/8f/168ba80e532dd6a93f96f8f706f1ad41d7990b6e1aeedc1cc0d211a33497/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8781a792a070cf2bd1b86d3aa943894115faaba6e88122a7bf32d62072742453", size = 11589, upload-time = "2026-10-02T23:05:33.251Z" }, + { url = "https://files.pythonhosted.org/packages/32/b3/aa2c95a574d3af39403a469b295886eb9b6d448da568cbebb5a2cbfdc2e5/markupsafe-3.0.4-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:971a3bbb75d97ae4e2e8f7d4834236f86f85f0c85e04ab2e191db1123b04f80b", size = 11801, upload-time = "2026-10-02T23:05:34.315Z" }, + { url = "https://files.pythonhosted.org/packages/60/d0/34b810107d83840e768bf485de795893ebbae35b26ab061b487adfa0a692/markupsafe-3.0.4-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:8909c2f1c6dd65e054ac4b573a91c8384d1492281e55d82d159d653f7a13adf6", size = 11447, upload-time = "2026-10-02T23:05:35.302Z" }, + { url = "https://files.pythonhosted.org/packages/6c/ab/2f8488f0f817a39fca068d2b17daf446bf5cdb3eae28c3720af534d873b4/markupsafe-3.0.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:4cf3468d5ec187ffffcaca8e61929a37448f215dafc1386a12c750a72fe53634", size = 11722, upload-time = "2026-10-02T23:05:36.363Z" }, + { url = "https://files.pythonhosted.org/packages/ad/40/e2d117b048d47282ade906fbfd92814cbee5647afc13fda88a3406039372/markupsafe-3.0.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:52704c5d36eb6dda8866493decd61111fff86244c9b1ad225ca01b9e91e5970f", size = 12061, upload-time = "2026-10-02T23:05:37.397Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a8/73a81135e85ba66217f5af7facb03bbb386807e1a729ab64532e4c802652/markupsafe-3.0.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1caa2fa5a6184fb233153b35f654e6687bd555476f6170f29d8ee9be1a8b0af9", size = 24434, upload-time = "2026-10-02T23:05:38.407Z" }, + { url = "https://files.pythonhosted.org/packages/ac/ca/fa9216dd01efee2dfdacafe7df32b4d0170fbac694b0c258a193d6e53999/markupsafe-3.0.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:387d8cd30e69b3f0a72877b9ae717033396404e19095b17fe89753a981fda44f", size = 27183, upload-time = "2026-10-02T23:05:39.581Z" }, + { url = "https://files.pythonhosted.org/packages/fa/4e/a469509e538d37af51103b17b073126973f2b1cbf197ff32c7ddf025cfe5/markupsafe-3.0.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:051417f74bcaaefa316276e0ff723f541616ca51043d070da00249d9bddd3e3c", size = 24813, upload-time = "2026-10-02T23:05:40.671Z" }, + { url = "https://files.pythonhosted.org/packages/8f/db/d7282caf7ab03af44d5d6fdbaa019b35c7d7f1c90588b839c07cba640d6a/markupsafe-3.0.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8e9f292fcda89b324f2f5c91d13f1424a153e40fc2756f38ee23b15835ff300", size = 23049, upload-time = "2026-10-02T23:05:41.864Z" }, + { url = "https://files.pythonhosted.org/packages/30/f3/b6a425206e6964efda6acee544d0eb01d1501784d0b8e2dcc74986f33b17/markupsafe-3.0.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:df1ae86ff54725a01fa1a0510b914ca53a161b7050be74f6204e24aded5971d0", size = 22088, upload-time = "2026-10-02T23:05:43.014Z" }, + { url = "https://files.pythonhosted.org/packages/ea/8a/84d3582fc1f0d5bd466cdf2eebf175e172158a6e70701aacec1de1b35430/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8965520ac587c94a4ac48b729be3d8b8de00af39699b17585dfb599babe77977", size = 23846, upload-time = "2026-10-02T23:05:44.098Z" }, + { url = "https://files.pythonhosted.org/packages/1c/65/db101cce51b7ba4864ac491a9859d297dd1adf0e55b103fee9db9c47c527/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:340cbb1957ba99929cbf19a75626d36ba1ae21d1730b287d1cf7f824a20c4fc7", size = 22674, upload-time = "2026-10-02T23:05:45.23Z" }, + { url = "https://files.pythonhosted.org/packages/e0/49/ddee9813d71db0c7a5c9d97c832125e6758a0c844777f1cf076569bb0e22/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:3a93d9616ddecfb393727a0041a562cf0b15a244e20f2bd25efc7949be4c4f17", size = 24104, upload-time = "2026-10-02T23:05:46.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/0e/7d8518d726726870a2399d69fd30d0fa36c5e57a2132c336b58d7c491073/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:d2e56fd3b00222722abfb3f5f0759ddbae4b90811b5ad4343c64030ad1bde70c", size = 21637, upload-time = "2026-10-02T23:05:47.48Z" }, + { url = "https://files.pythonhosted.org/packages/b4/b0/b505e8a361ba557dbf3b3aa7331ea39b00d2022a26e925ff8463b9714bb3/markupsafe-3.0.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:0d9c47709875fdb321452056622e930c52afbc07a7d780762fbb8b4d91ce6fa4", size = 23081, upload-time = "2026-10-02T23:05:48.611Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ea/9cc3cea873f980c75cbdb6f4277ce30ee955de38be0b3d02f14c108e0698/markupsafe-3.0.4-cp314-cp314-win32.whl", hash = "sha256:38fc55594dab834470b6733dead2ee9e3f657fb0608c769dcafa0ba5ab52f45c", size = 14272, upload-time = "2026-10-02T23:05:49.707Z" }, + { url = "https://files.pythonhosted.org/packages/80/f0/5792ff768a410f93ee3f84fc19345295ffc352d2c936b424cb37e514714c/markupsafe-3.0.4-cp314-cp314-win_amd64.whl", hash = "sha256:c1bc67752d5f21013cfe430df4062441714eab79f65a6a05e01505957e9c35fe", size = 14501, upload-time = "2026-10-02T23:05:50.788Z" }, + { url = "https://files.pythonhosted.org/packages/5f/cf/3d074a8edffcc6899355232ff2543ae8d929733239596423b7db79698bc9/markupsafe-3.0.4-cp314-cp314-win_arm64.whl", hash = "sha256:7e1636da3d8dfc220b6dd10264db5f2b165e4888c4518594898fbe381049af8a", size = 14364, upload-time = "2026-10-02T23:05:51.857Z" }, + { url = "https://files.pythonhosted.org/packages/d9/31/87ce42159aae2163cf3bbbd0c44bc87780510eecab1ea3859099aed95dcb/markupsafe-3.0.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:805c8b84534fa10891890f0e4be39f3a99e94615d93e8836bf9fa1fdca2feeb2", size = 11759, upload-time = "2026-10-02T23:05:52.951Z" }, + { url = "https://files.pythonhosted.org/packages/5f/53/b047207eeb7752e960aca3eb1df5fb7eefa7dd4c62ac49bb156456c8a702/markupsafe-3.0.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:fa95848c929b6a75f6848d3c9793e59db365ee436776e57db835cdbfa79ba977", size = 12096, upload-time = "2026-10-02T23:05:54.066Z" }, + { url = "https://files.pythonhosted.org/packages/ee/51/4326c88a13c7b755657d44b4bb986f8c3d9843ecba7e22d98661d87f9a57/markupsafe-3.0.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e916035e3e9930cbdfdd10abf48861340221857f45509565898e012263f7b289", size = 25699, upload-time = "2026-10-02T23:05:55.15Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/990581b7474bfcf2cf34bed6ba5ea23bd87adb9d671213d68e88620e7a6b/markupsafe-3.0.4-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:b4d12837e0203bbace818ff4a7461afdcd78bcd782351cea148139180d7bcffe", size = 27804, upload-time = "2026-10-02T23:05:56.29Z" }, + { url = "https://files.pythonhosted.org/packages/6b/89/89491878c28e8291f5aa2fffe2c2d57230d10ae366d55dd810b840513d78/markupsafe-3.0.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5086f9975abb1ab531ee6afca1761e4b59a19b446f3f6522ed776963228cfe5a", size = 25912, upload-time = "2026-10-02T23:05:57.416Z" }, + { url = "https://files.pythonhosted.org/packages/30/77/680998b54efdea06fc114565cd739b6d059f826a0279219b218dfa750d29/markupsafe-3.0.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b4a635a0487774f841cb1fb62e907e7195cc95bc761e053184b8acc3ceb20733", size = 23623, upload-time = "2026-10-02T23:05:58.557Z" }, + { url = "https://files.pythonhosted.org/packages/ae/75/2709f5ac5de9467b40b10e2bb8f89cc63dfb74582e09aa734b1124a217de/markupsafe-3.0.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb96e6e088d6cf71c1ea977510948320234824cf226e32f6f6e044f7a9c82b34", size = 23301, upload-time = "2026-10-02T23:05:59.94Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c8/39eadc6c5b14c9c7679bfb98f4d4c6a97863b5beb91839aca4d2d6e16e55/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8b5d563170ff8ba3181caa967c99a3c804d1dedb702c7cb93a6a7c32247da978", size = 24950, upload-time = "2026-10-02T23:06:01.289Z" }, + { url = "https://files.pythonhosted.org/packages/1a/5e/01037f8a43e8ccb0bffb4fbdc5212db05bf080fdd7286cd392332d58128a/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:396ec4e65cc889f69786b3b89478b471cee5a3bcf468b9d9bb03e1a30fb291fc", size = 23330, upload-time = "2026-10-02T23:06:02.441Z" }, + { url = "https://files.pythonhosted.org/packages/d4/f4/23e83ce0596bb0cbe670502d31df8f757bbd01a392aa486fa3b40d1ed399/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:15ba9e28640feef770374b116a6f019c21f52404aeabe516aa7f800587b98cfc", size = 25239, upload-time = "2026-10-02T23:06:03.579Z" }, + { url = "https://files.pythonhosted.org/packages/88/5b/3708897368073cc683d524750474f41a77d2986152c380dcc55b20fdf340/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:d920abdfa61279ba1a2ef9484aab07bf03331f8c08a10120fa332353d06e6932", size = 22767, upload-time = "2026-10-02T23:06:04.699Z" }, + { url = "https://files.pythonhosted.org/packages/c6/61/ebda1307864b409e6b3115757a3d4a09cca46cfb6cc65191b5de226b424b/markupsafe-3.0.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a9f54054101545a9a9cccefddf54316aa6e4491611fcbef9e91b3b6bebec04f6", size = 23668, upload-time = "2026-10-02T23:06:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/09/15/98075cceac3b5ba0dbb8e4762a847be967d2befc349a2cf2d0ac77f62c9d/markupsafe-3.0.4-cp314-cp314t-win32.whl", hash = "sha256:12a606a492de952afcb43b59a14aaaaad120e708d3663dd0fdf2d738d427a691", size = 14301, upload-time = "2026-10-02T23:06:07.109Z" }, + { url = "https://files.pythonhosted.org/packages/0b/a3/768b560fcc4156685cb563d922b217810cfa7bc135773367f62f1f9d2078/markupsafe-3.0.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a18f38cafc329bac5e3c2b96c765b4c96d3d103421ed22ab7988c1e3fce27464", size = 14526, upload-time = "2026-10-02T23:06:08.276Z" }, + { url = "https://files.pythonhosted.org/packages/93/63/da554b4c97a6b0ea3229ca7fe8cbfb620be81613d517f482e85958550537/markupsafe-3.0.4-cp314-cp314t-win_arm64.whl", hash = "sha256:eba154571c16e032112afac0dc2dfe9e63c2ceb7aedd07bb7eecf2ce26d4dd4c", size = 14421, upload-time = "2026-10-02T23:06:09.402Z" }, + { url = "https://files.pythonhosted.org/packages/a9/30/54d11c8ca027114898cab97421fb39e4ffd9ddf47cdbc44df2ec76722da9/markupsafe-3.0.4-cp315-cp315-android_24_arm64_v8a.whl", hash = "sha256:737c9c3981998eba27f11786f84fddcbabc74068b72a4a1f454ea02094b57b65", size = 12791, upload-time = "2026-10-02T23:06:10.485Z" }, + { url = "https://files.pythonhosted.org/packages/10/6d/97c913e253a14bd3cd0e15a5c56d13203b823fa7ee32498342896a072dc4/markupsafe-3.0.4-cp315-cp315-android_24_x86_64.whl", hash = "sha256:489505b03f692c3f376394e49194fa7a7f9e8558d6e293a7056a0032b0c38163", size = 12527, upload-time = "2026-10-02T23:06:11.834Z" }, + { url = "https://files.pythonhosted.org/packages/26/f9/b86d032042a4d597d9e1997f0e5f63a3eedaf11258e0a05760b0a0a826ea/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:077293e425f28ec737dbcad442a71752e28f8ae27cde3d68acd1fb212091cd92", size = 11602, upload-time = "2026-10-02T23:06:13.122Z" }, + { url = "https://files.pythonhosted.org/packages/f2/dc/73c14c1eedf0ac5fa3292ba43435e6c49d2c2050f33cebde541f8f4807f1/markupsafe-3.0.4-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9348cbb300d224fe3b89793262cb093504d4ae927004468463f745188a193e4a", size = 11817, upload-time = "2026-10-02T23:06:14.227Z" }, + { url = "https://files.pythonhosted.org/packages/8f/69/2c2fcaa5fcee22d72c7819c0d536fd181c74a688e6143845419579cd2863/markupsafe-3.0.4-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:b807e598953730f82e4eae3bd30f6a122cf6b31c398c6b504c0e04c13c170429", size = 11446, upload-time = "2026-10-02T23:06:15.574Z" }, + { url = "https://files.pythonhosted.org/packages/88/54/9e5ec76c62e6e2834d5a93623018c943e8b3bb41d663e3fd4c03303b9b85/markupsafe-3.0.4-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:799c39bdf5e2f1292fedd3009f7b3c9e760f10b2420cb9638d56920840ff6db8", size = 11719, upload-time = "2026-10-02T23:06:16.701Z" }, + { url = "https://files.pythonhosted.org/packages/96/24/3ec292b44064c16229e064d770b2625bd8ea941aa61f44905a9fa44942c0/markupsafe-3.0.4-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:ae9dcb8fbe244cb82f8a6458b455b927a03685e383d9bacf1ea5ce180b96dc97", size = 12075, upload-time = "2026-10-02T23:06:17.855Z" }, + { url = "https://files.pythonhosted.org/packages/aa/85/b64fdb1f304848518742136983c24e96d967bfb59a0ea160e92736901ab0/markupsafe-3.0.4-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bced6e2a6dba6a28f7dd3c6ce14df1b2dd495923f16ea484cad03decd463b2b", size = 24656, upload-time = "2026-10-02T23:06:18.963Z" }, + { url = "https://files.pythonhosted.org/packages/9c/18/23997d4c65b355da6390d61cd56e0ab3befd6ba8dda25cb40c602bd0fa6b/markupsafe-3.0.4-cp315-cp315-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3882fb412298575bae3b9c46868251f15cc69307359f87bb1b382e53d6e5a2c9", size = 27737, upload-time = "2026-10-02T23:06:20.117Z" }, + { url = "https://files.pythonhosted.org/packages/d4/36/35998dead3c6af88c38265a56e58100211f036234ab88eb2283fd4cbce44/markupsafe-3.0.4-cp315-cp315-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:04e7902ba80ee4bac1d50a549606527a1dcf0476cd81403db41099d3b60ec653", size = 25003, upload-time = "2026-10-02T23:06:21.284Z" }, + { url = "https://files.pythonhosted.org/packages/82/96/ef49135ce260db4ca4a12b119ed468449cd248db6b1468e2112b546d7a2e/markupsafe-3.0.4-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:925f929d6b59a8b3f8b8c6ac363cd0af7eecc81efb3071770b3c6717c450a369", size = 23111, upload-time = "2026-10-02T23:06:22.524Z" }, + { url = "https://files.pythonhosted.org/packages/50/7d/83126e338bd88c17a220668235368ad719fd4638e426739858cbb8508f77/markupsafe-3.0.4-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f68edfc67aabac33708941f26f22a7b8e9f81429bc0cf249fcf7d66b23af8d19", size = 22403, upload-time = "2026-10-02T23:06:23.785Z" }, + { url = "https://files.pythonhosted.org/packages/83/dd/daf7e420de23c8206c365204e7b85e1251d8e19d34196a56336f316e5ed2/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:e5c802729725bd07e2bc3ab7b76dc7e0bbfc53129d8f1eb1c002c24cf774717e", size = 24083, upload-time = "2026-10-02T23:06:25.037Z" }, + { url = "https://files.pythonhosted.org/packages/19/3c/11eecdc06bc44ad5570350085b572ebf049e8f9a38d1ece6d76640b739cd/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_armv7l.whl", hash = "sha256:55ffd6ce583d97dc71dc92e930324c8c0d25aea7e3ade6ae54ef77cedb096811", size = 22931, upload-time = "2026-10-02T23:06:26.328Z" }, + { url = "https://files.pythonhosted.org/packages/0d/9e/ac0fd77f2a726e56ecc3ca0235d095feace1358d1b822406c2a2ef26a4dc/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_ppc64le.whl", hash = "sha256:2cb3dd71fc6be918ad4264346a8ed69485f9b7ed7bf35495d8e22807cd6b8bea", size = 24299, upload-time = "2026-10-02T23:06:27.742Z" }, + { url = "https://files.pythonhosted.org/packages/d7/09/c6bd842ad58ff5b3bc76eeed7e9a42a6f11adc5d090ec697b72c9672731e/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:94f5407f7bc64fa6463906b896f9904beeeb7dd8dc116ee8e9056c8714ff9916", size = 21948, upload-time = "2026-10-02T23:06:29.274Z" }, + { url = "https://files.pythonhosted.org/packages/a3/46/82f586711fed61e86faa1ee1bc317d68cd45a10c8bdbe3f7d1fdf9026ad8/markupsafe-3.0.4-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:2dad610540cb2e6272855c178f08ae9a1c7ac258a7fb71660553a5f104b42741", size = 23150, upload-time = "2026-10-02T23:06:30.583Z" }, + { url = "https://files.pythonhosted.org/packages/19/2d/2dfdce99318abbfa26925195fbc17db188c46a1ec6457be121b6f9cfeb42/markupsafe-3.0.4-cp315-cp315-win32.whl", hash = "sha256:03470d1a8268e692ecf79ecd565593e59d44219377a7ead61f1f1b94c1f7ff6b", size = 14269, upload-time = "2026-10-02T23:06:31.949Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ec/6000fd82e8791e58fcd0456ec20f098957e2b03d5ed02eb73241a577c0ba/markupsafe-3.0.4-cp315-cp315-win_amd64.whl", hash = "sha256:d882a373d8093c2941e01291b7ced96e9cbe4781da9a7751ca7e6c70385e5214", size = 14496, upload-time = "2026-10-02T23:06:33.258Z" }, + { url = "https://files.pythonhosted.org/packages/bc/66/e73bd5016421d5d6e2fb6de7dd609f9de020942ac8c626526bd8c6eeaf82/markupsafe-3.0.4-cp315-cp315-win_arm64.whl", hash = "sha256:353bd63081912ab8cfa6a0c7d185934cdf8426f04c618bba6bc4b394f2069b67", size = 14361, upload-time = "2026-10-02T23:06:34.539Z" }, + { url = "https://files.pythonhosted.org/packages/90/df/cb8c3dc98d313a951df2f8968f44e4cb5643df6d3cab749a530ce2f7d972/markupsafe-3.0.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:c61750fadcd119d0825bcb7d7d675dd264dcc89cc05292aab5be68ebdbb374ad", size = 11760, upload-time = "2026-10-02T23:06:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/d6/bb/4af9b3ca0753d654ac75f9531d5bd741bb77ca6e696f36807c475ffc099a/markupsafe-3.0.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:1c0df495a977d10460a94941799c72d5b5ab03d3858d949b55b5a66c8f371c99", size = 12108, upload-time = "2026-10-02T23:06:37.089Z" }, + { url = "https://files.pythonhosted.org/packages/3f/d4/b56429313aee5fd59b079c3df5615299959e25e7113eb6d8caadbdd7d38a/markupsafe-3.0.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02fa4acbc6a3fc5c693c34d4dd8c1130b7fe99cc915181b0ddd6f72aeb296002", size = 25762, upload-time = "2026-10-02T23:06:38.419Z" }, + { url = "https://files.pythonhosted.org/packages/65/f5/34c181e891aa4f7d59c918584672e0c5eb7fffe76c1387d1246008bf4081/markupsafe-3.0.4-cp315-cp315t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:05295589e619b9bed252a86b532b8e27350abc372d18ba89b59375325e91ec1e", size = 28400, upload-time = "2026-10-02T23:06:39.819Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b5/ad14694fd0ac9a5ce30bc6498f2999378f418583dd1679cca5a1b512957e/markupsafe-3.0.4-cp315-cp315t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:be6cb0c799abb0e2ba3e618e6d28ddddf7e485f6c2ce938dfa237daf3905072c", size = 25946, upload-time = "2026-10-02T23:06:41.381Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a8/26b606445387d0ceb1eb1f21840094b84e4e3c3c3983d80d10b89823b490/markupsafe-3.0.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:26e9867520db70d37f7fb421a7f0d8adb40171011fb84ce869afa1a83370dfa8", size = 23634, upload-time = "2026-10-02T23:06:42.748Z" }, + { url = "https://files.pythonhosted.org/packages/39/a2/b8814de672f1f0094d498bf646f2fec9d6356b503d28ef500b71c5095377/markupsafe-3.0.4-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f03460ff076f70ab595bb45a0205ccea1971443575b6920c52e755dec2b3fbfe", size = 23488, upload-time = "2026-10-02T23:06:44.176Z" }, + { url = "https://files.pythonhosted.org/packages/db/c7/287223376fb73335a3cc5d6eb22c6ab01358cf33945a9c39c06b9dac3f4b/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:436e3ffc6310d3c41878c601db29098102fe5d8a467c49da4a4125254e0980f2", size = 25037, upload-time = "2026-10-02T23:06:45.646Z" }, + { url = "https://files.pythonhosted.org/packages/f9/29/4df8355e313426d19e62ba33e0253c009ca12a0894ee77d67fa67255361c/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_armv7l.whl", hash = "sha256:4e2c4809c14559aa7ef426f27fb35afbb38104c349a903bf8f3600456764bb38", size = 23543, upload-time = "2026-10-02T23:06:47.264Z" }, + { url = "https://files.pythonhosted.org/packages/71/e5/8377731e8495668dcc768f645e717df18318c841edaf023a99395f6da9b4/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_ppc64le.whl", hash = "sha256:da2af0d7aebfc2074080d72efa6ab8317c62481ef1f896f65d9999c1c01f4494", size = 25282, upload-time = "2026-10-02T23:06:48.795Z" }, + { url = "https://files.pythonhosted.org/packages/ed/5f/373456e37ceb1478d657d6fe769cbe0a39f0a8dfc1548eeb19c471eefdd9/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:aa2c838cc024642cc04c6854232f32b43e5e22833dd11119c1766c7873b8370d", size = 22993, upload-time = "2026-10-02T23:06:50.31Z" }, + { url = "https://files.pythonhosted.org/packages/d7/93/2cbd5628435afb6f541bbaced4bce0c2edac4b09a142e6e928b8b0da9858/markupsafe-3.0.4-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:b91cc9d336957239ff200f30097e6fea2dc6d6fb3c81e853eaa09eac904fd894", size = 23712, upload-time = "2026-10-02T23:06:51.759Z" }, + { url = "https://files.pythonhosted.org/packages/81/99/157e10966b033b363aeda5263e82596ee232a0b1d082fdbf90aa417ff083/markupsafe-3.0.4-cp315-cp315t-win32.whl", hash = "sha256:e49fb0d1ce92cfa0cb198cc5b1b11cdf9d0638658e2a2db2687e39db7c87fc78", size = 14300, upload-time = "2026-10-02T23:06:53.241Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/55884815414c9706a23deca150b72c25a62109e65b0b6ce232077802c719/markupsafe-3.0.4-cp315-cp315t-win_amd64.whl", hash = "sha256:4f6e0852a0283b1b1fd776eeb7b766a5f440b3e2bd31ab51af3b400585f3965c", size = 14522, upload-time = "2026-10-02T23:06:54.729Z" }, + { url = "https://files.pythonhosted.org/packages/92/f9/ecbde7149e95b8a0f18e16d5d747f7dc06049d5da2e4f77f6f5e4a1f46a8/markupsafe-3.0.4-cp315-cp315t-win_arm64.whl", hash = "sha256:39dbacefc411633db5b4378b066a9aca70a3d7e2922c9e578d825f844026eeba", size = 14417, upload-time = "2026-10-02T23:06:56.246Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ab/27/79acfd3d6f526a4a16f3d209dedcf7b5bc1ea15c5bf2bbb82f2c2285057e/nodeenv-1.11.1.tar.gz", hash = "sha256:d7fa027c1df437bdd974b0502a8dc76fa1b3183e369ac9539602bc6bab9100ba", size = 99853, upload-time = "2026-10-29T20:54:04.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/35/a09e0b1dac4027e35b872e4f123a9f9e7bbc39e6424bdc9776920d3a6491/nodeenv-1.11.1-py2.py3-none-any.whl", hash = "sha256:bc0fb085d8ed0d67a6471892535f3d9b795eb911b7cd129d3a24967aab7a538f", size = 34473, upload-time = "2026-10-29T20:54:02.752Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/40/28/1c058a9143d2304b3078f8dadcbb9dc27f7d3581b5d05157c84869c4cc0d/platformdirs-4.14.0.tar.gz", hash = "sha256:83f009c1e8c5fc36869b5171755b291887bd2cade7965233cdfabd23d754720b", size = 70387, upload-time = "2026-10-28T18:27:30.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/2b/887969ef4bb53e0cb271dc7c3f4a73d50c5d58bb88aeaae311e3340cd57e/platformdirs-4.14.0-py3-none-any.whl", hash = "sha256:1dadfc59a8a5d7565b3d9270ae8fa71c065595352c4277b41c11ee6c222c4b91", size = 35259, upload-time = "2026-10-28T18:27:29.659Z" }, +] + +[[package]] +name = "pluggy" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/db/7fc19e6f2dc92a966727031389fc2e08b558f0f25eb7403c1119ad4713cd/pluggy-1.7.0.tar.gz", hash = "sha256:d1eaa46ebb595891b860ab086b4d09c8588af65ebd4361b8e8f4bb8920b90ba8", size = 123304, upload-time = "2026-10-15T09:50:58.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/9e/2b38731e0fc536806f16490e1a12d7f0dc2a1235aa8cc07bcc75416a7daa/pluggy-1.7.0-py3-none-any.whl", hash = "sha256:7dd7b0d8832ba3cb632c306926ded123429211b83641b35dc5c41ad2d34f9bec", size = 27082, upload-time = "2026-10-15T09:50:56.808Z" }, +] + +[[package]] +name = "polyglot-sql-chio" +version = "0.13.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/8c/b48d0fcca36c2e6df0a2ab8aa24e029373264b6dfa9db683eb533f6b6249/polyglot_sql_chio-0.13.10.tar.gz", hash = "sha256:4365617889c3779fd1a3ebdd15ab03c92c517cb1be36dedf82e5853b31d39781", size = 2279296, upload-time = "2026-10-28T23:07:03.083Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/7a/c66b36a40c0931d9647215f315f90e2e0d83328165dce7275d624f694381/polyglot_sql_chio-0.13.10-cp312-abi3-macosx_10_12_x86_64.whl", hash = "sha256:a1abe78cde4b54d145f42356ca4c3a7585775aa4efee954209084ea993633f90", size = 13701198, upload-time = "2026-10-28T23:06:50.006Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e2/b2ba58e445163b9e29a715d9e3eab5fd9e276e8bbe58f5710bd77aae70d7/polyglot_sql_chio-0.13.10-cp312-abi3-macosx_11_0_arm64.whl", hash = "sha256:a7d01559916bff555fd0c258f4a52a98e3f2fa25b1f4c482ef35cb868704c3b6", size = 12941723, upload-time = "2026-10-28T23:06:52.738Z" }, + { url = "https://files.pythonhosted.org/packages/c6/08/a9e5cb57c9938551edeb0884cc1b57ef54bbb38eb5f371bb146b663881ab/polyglot_sql_chio-0.13.10-cp312-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e5ee843dccffe87501113238ba333c17dce55b31867fcdc1b81e245b9c3eb4", size = 12895624, upload-time = "2026-10-28T23:06:55.52Z" }, + { url = "https://files.pythonhosted.org/packages/a0/9e/0879b3a267899906c231270d0167d8c32c14dcc2ae346b86834c4f7bb8b2/polyglot_sql_chio-0.13.10-cp312-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd2ca982a19c7fbeb460914eceae60d9c5f7e6e631473f0141585b778772c1ce", size = 12661001, upload-time = "2026-10-28T23:06:58.287Z" }, + { url = "https://files.pythonhosted.org/packages/e7/13/dbdbce022153042edb59abe07e64c51a0e0a9a8f4c86cf2043f1c42a1da7/polyglot_sql_chio-0.13.10-cp312-abi3-win_amd64.whl", hash = "sha256:f366391d63fbf455f71c07fb03e4ac1cf860609de7fd875759647ffe98659320", size = 12301030, upload-time = "2026-10-28T23:07:00.945Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3a/c3/6965c2690ddce885ec75121572d065a5b94c5a5d8d3e657927ecda9fd715/pre_commit-4.7.0.tar.gz", hash = "sha256:1f3deee12b914c8fa184bd2b5953391c2e4f36763e403d4a23dc9983fa7c6e86", size = 198120, upload-time = "2026-10-12T20:45:40.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/83/ce0a196e4f7b20b11ce3aa1fc72fb973f64b7628d51c1b8aebad37fb7f81/pre_commit-4.7.0-py2.py3-none-any.whl", hash = "sha256:2e229038ad3656081b70c27913157e4b636ef306f643f024a8a8e3f0f7b2f877", size = 226261, upload-time = "2026-10-12T20:45:38.858Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.53" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/ea/39b988c938f75cb75d7045b5c69f8bfed47ee2152c8837fb403de29d6fb8/prompt_toolkit-3.0.53.tar.gz", hash = "sha256:9ec8a0ad96d5c56148b3f914aa79c1564c3fde5d2e6b876e7bc327e353cf8fa6", size = 435492, upload-time = "2026-07-26T20:56:14.758Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, +] + +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + +[[package]] +name = "pytest" +version = "9.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/42/bb4641652e58dedeac2ec51abc1f8cae4fee4ec63899a0999016229a0f2a/pytest-9.2.0.tar.gz", hash = "sha256:97c4f3b1f3c7414aa36465e09b8220f1d03627993bd51b1282e193753c80557b", size = 1797555, upload-time = "2026-10-27T21:30:20.728Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/39/1f5d8c8877a0fa327db959c19341d4be61660de1ab6b7391f9eb805bcd67/pytest-9.2.0-py3-none-any.whl", hash = "sha256:07acdcf88ec174f907dc01af70bf52d49b014bbab03faeeade31c96b18c2e588", size = 411776, upload-time = "2026-10-27T21:30:19.131Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + +[[package]] +name = "python-discovery" +version = "1.6.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/0b/908da9ef8a14b600fc188eee8d63ede071996114613721153f6fc9ea3cd5/python_discovery-1.6.7.tar.gz", hash = "sha256:7f7c28568c8ec3d988d8bddbdbec8e65f08e8767e52f734e18b8b498549b9b70", size = 117807, upload-time = "2026-10-28T06:24:21.878Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/e4/1c7c388e874222c28afb92fe3ebfeebe55555276523ba7aab0a82a10b2b5/python_discovery-1.6.7-py3-none-any.whl", hash = "sha256:5422f67e5f533bb02d7b73de1c422ed74037536fd869e05156c07565926ec68e", size = 39156, upload-time = "2026-10-28T06:24:20.462Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + +[[package]] +name = "questionary" +version = "2.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "prompt-toolkit" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/45/eafb0bba0f9988f6a2520f9ca2df2c82ddfa8d67c95d6625452e97b204a5/questionary-2.1.1.tar.gz", hash = "sha256:3d7e980292bb0107abaa79c68dd3eee3c561b83a0f89ae482860b181c8bd412d", size = 25845, upload-time = "2025-08-28T19:00:20.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/26/1062c7ec1b053db9e499b4d2d5bc231743201b74051c973dadeac80a8f43/questionary-2.1.1-py3-none-any.whl", hash = "sha256:a51af13f345f1cdea62347589fbb6df3b290306ab8930713bfae4d475a7d4a59", size = 36753, upload-time = "2025-08-28T19:00:19.56Z" }, +] + +[[package]] +name = "ruff" +version = "0.17.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/59/73/81f140c4edd9cc323de5478e6bbd3bbcfae9a82692df1a258de8cec6a6d2/ruff-0.17.3.tar.gz", hash = "sha256:a57049c8addb3662073859bb55a7ad35aa54f8a7c9ed268911d1333c5aadaaa9", size = 5339444, upload-time = "2026-10-27T00:34:05.914Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/8c/083365310c4e79c842f25bf5a842facbd9b0c0ec336f96dffa5074fe723d/ruff-0.17.3-py3-none-linux_armv6l.whl", hash = "sha256:ab86b84cd9d05e56d329742c0cabdcbe2d1d56c98f4a04aab9b5b66196febfb4", size = 10216522, upload-time = "2026-10-27T00:33:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/72/26/4b2edf31cfebe9605d8f6ed8f1d6f376d3a53d4c9a123e62661c64c440f0/ruff-0.17.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e407b4a6852853a9a2cdce96963e94ad9775578bb44e7561a0f2829f3cc04b11", size = 10217786, upload-time = "2026-10-27T00:33:33.949Z" }, + { url = "https://files.pythonhosted.org/packages/a2/7e/537aba4b7f54397a374d2f6a6fc93e9c8015e18553f31a4a7b7dbced48b5/ruff-0.17.3-py3-none-macosx_11_0_arm64.whl", hash = "sha256:b75cc113b3aca388933d8296b667b1ac401a80317ea3074c9b0a66541199da29", size = 9440362, upload-time = "2026-10-27T00:33:35.784Z" }, + { url = "https://files.pythonhosted.org/packages/3d/66/283e39734f66c8a5ebaa93c22eb7b7515779a0b1d5a5c0ad20e05c0edb41/ruff-0.17.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ab5fa7ed5ce18554ea69d850ad162cbaa0c85fc47c08d83a569f806681c9a6ad", size = 10094281, upload-time = "2026-10-27T00:33:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/21/4c/1ffc768497bd025a32c72152771e40c0b99bddc5cc6e4f50c7949a395346/ruff-0.17.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c673c170e27834d1262e3a9c1e97bdc9b9c3b180c63d89173ce69747232474c1", size = 10022543, upload-time = "2026-10-27T00:33:40.181Z" }, + { url = "https://files.pythonhosted.org/packages/73/c3/f90db547301080fcb82c92abf914c8bd57b89911d9b65a6c4cc6b52d0635/ruff-0.17.3-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6a754c0341bc9ca151b7b335988b3263921c08859f84ca219b70cee825571b6f", size = 10683921, upload-time = "2026-10-27T00:33:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/e5/98/92d4a8a26c6c96cf1a5b34dd59036d8a71e52d650aab065c58867e964e63/ruff-0.17.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4590b9d86c0b1543ebb09094254e191cd43fe8edaf330baa035aab03da251532", size = 11373408, upload-time = "2026-10-27T00:33:44.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/02/1f24d2e3064c9753926deeb901afa57777b37ee46799b09ddd7aee6e5145/ruff-0.17.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bbde18beccd98c29a227c63c99022e3443b99258e08326df6425dfe0443705ef", size = 10977469, upload-time = "2026-10-27T00:33:46.179Z" }, + { url = "https://files.pythonhosted.org/packages/f0/20/fecedea2209f574ab92033b344e01a7f0c2dc76c6d6d19a14c9f8141173f/ruff-0.17.3-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7fa4b0f931cdbcb8a52044f370a3eb429fb42eb88807b0e800fcaa98dd42b24e", size = 10577491, upload-time = "2026-10-27T00:33:48.233Z" }, + { url = "https://files.pythonhosted.org/packages/33/36/b93fdf0bc6f84f0cd487aadfb0354deb40013669ea35ac3d26855de1e959/ruff-0.17.3-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:39405f341deec934b4289634324ad695f72df2970f9121582872eaf5b722f0f5", size = 10608748, upload-time = "2026-10-27T00:33:50.284Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b7/7d3f5f14145daead1efb0b9a8289ed1daa100f419d60774375951723ce00/ruff-0.17.3-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:68074094f55ca3d29ab19491f640282f7a84061c4c7977aa5094b968ca457e9a", size = 10075057, upload-time = "2026-10-27T00:33:52.074Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ed/93ff7a5909ee56e2bd4b982deae9e0efb2724417874a90c9e41ad66c6dd7/ruff-0.17.3-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:27bb571ac78fef178fe5118d2827df95c2aae74a48fff2b15f6acd03af5ff5bb", size = 10022431, upload-time = "2026-10-27T00:33:54.061Z" }, + { url = "https://files.pythonhosted.org/packages/9b/07/a83b9eeec8c1c3c0e4eda15f06c3a91001bfd2687ea2f07b750719bf9712/ruff-0.17.3-py3-none-musllinux_1_2_i686.whl", hash = "sha256:029c37cbd3de48ca37aa12117bead2d065f15da17d66bf84e0d4274013705db2", size = 10394903, upload-time = "2026-10-27T00:33:56.158Z" }, + { url = "https://files.pythonhosted.org/packages/af/b2/05c39857c2526fcbebe46ad93a8dc97424656dfef14d3ef933abe1ffd331/ruff-0.17.3-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:692fdb46d0fd9c71e380947dc5fc72f2c3f881073bc1f739e4579376231affbe", size = 10799780, upload-time = "2026-10-27T00:33:58.204Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d9/47171a16739eb7a353936b31c20cf54340531e68c9af7d72fd7a784e235d/ruff-0.17.3-py3-none-win32.whl", hash = "sha256:46539cb031b01fd29ae0a02142483c46640b233ee12bb1c1b690bffa1555d5ca", size = 9908481, upload-time = "2026-10-27T00:34:00.087Z" }, + { url = "https://files.pythonhosted.org/packages/d0/a0/f08c1631bfd81faa6a90eaf70c37c786c11bed2046302b56bb506c25e1a0/ruff-0.17.3-py3-none-win_amd64.whl", hash = "sha256:56b8ffb3f3b3cbccb8b76b71b38261b7ed982ef5d20f5b447b1b61a7f4949b24", size = 10341333, upload-time = "2026-10-27T00:34:02.099Z" }, + { url = "https://files.pythonhosted.org/packages/99/d5/4ad4ed31413c301430918e70d3d38ae6f097af0766cb9ab8c9d502b2a4d6/ruff-0.17.3-py3-none-win_arm64.whl", hash = "sha256:81fce74bcd6be0e9da7c67c7553fd0f1f12964a2052cd728ef917b937a5ca1d9", size = 9854332, upload-time = "2026-10-27T00:34:03.847Z" }, +] + +[[package]] +name = "sqlclean" +version = "0.0.1" +source = { editable = "." } +dependencies = [ + { name = "sqlclean-cli" }, +] + +[package.optional-dependencies] +ast = [ + { name = "polyglot-sql-chio" }, +] + +[package.dev-dependencies] +dev = [ + { name = "commitizen" }, + { name = "fensu" }, + { name = "pre-commit" }, + { name = "pytest" }, + { name = "pytest-xdist" }, + { name = "ruff" }, + { name = "ty" }, +] + +[package.metadata] +requires-dist = [ + { name = "polyglot-sql-chio", marker = "extra == 'ast'", specifier = "==0.13.10" }, + { name = "sqlclean-cli", directory = "packages/sqlclean-cli" }, +] +provides-extras = ["ast"] + +[package.metadata.requires-dev] +dev = [ + { name = "commitizen", specifier = ">=4.9.1" }, + { name = "fensu", specifier = ">=0.28.0" }, + { name = "pre-commit", specifier = ">=4.3.0" }, + { name = "pytest", specifier = ">=8.4.2" }, + { name = "pytest-xdist", specifier = ">=3.8.0" }, + { name = "ruff", specifier = ">=0.12.0" }, + { name = "ty", specifier = ">=0.0.1a19" }, +] + +[[package]] +name = "sqlclean-cli" +version = "0.0.1" +source = { directory = "packages/sqlclean-cli" } + +[[package]] +name = "termcolor" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/46/79/cf31d7a93a8fdc6aa0fbb665be84426a8c5a557d9240b6239e9e11e35fc5/termcolor-3.3.0.tar.gz", hash = "sha256:348871ca648ec6a9a983a13ab626c0acce02f515b9e1983332b17af7979521c5", size = 14434, upload-time = "2025-12-29T12:55:21.882Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/d1/8bb87d21e9aeb323cc03034f5eaf2c8f69841e40e4853c2627edf8111ed3/termcolor-3.3.0-py3-none-any.whl", hash = "sha256:cf642efadaf0a8ebbbf4bc7a31cec2f9b5f21a9f726f4ccbb08192c9c26f43a5", size = 7734, upload-time = "2025-12-29T12:55:20.718Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.15.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/08/72/fc01563df95c189a9f65850096bdc205bc2fe37da6c740cd64afaf18d07e/tomlkit-0.15.2.tar.gz", hash = "sha256:83e32755ba1d20d9340bd069ee97e9f6232ba2346b63ffcba425de01f7e327a6", size = 190522, upload-time = "2026-10-16T04:07:08.166Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/da/46/7d84138a60a8bd796825b2d38ebe92d0d16501ca57d501b7c5d677401bbc/tomlkit-0.15.2-py3-none-any.whl", hash = "sha256:05562412163a3037b94497385fb7f9204c637bc605af7a48199e41cf2272f437", size = 52225, upload-time = "2026-10-16T04:07:09.216Z" }, +] + +[[package]] +name = "ty" +version = "0.0.95" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/39/08/d63954788578b9189dbf85081d8cbdba9a32a9a6a29729129a0da09cd3a0/ty-0.0.95.tar.gz", hash = "sha256:a480853a74d43074b2d4b28025d406b9d7223f7fe9f3a6c2adec3eacdbf14ca0", size = 8115525, upload-time = "2026-10-29T19:21:51.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/c8/8f912c9d91503aa26b704838e2ecba448fae5713a799e2022e03e20d5186/ty-0.0.95-py3-none-linux_armv6l.whl", hash = "sha256:4e0745137dbd472027e71827ef9dec803439395319a182835fea2bb7ccc264cb", size = 14526217, upload-time = "2026-10-29T19:21:11.573Z" }, + { url = "https://files.pythonhosted.org/packages/49/6b/25feb9b4c04da26d2aa91527b0cc3de281aa45c9857abb900f1c4275e1aa/ty-0.0.95-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3b9587b262312f94bce6b8c139db0542c44a1dce8c2224b4d33ab649b287b119", size = 14099685, upload-time = "2026-10-29T19:21:14.433Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6b/2a084a72c8069f8496fcc0f576ec053f648b1baeed07532b3eb84c4982e4/ty-0.0.95-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3e7902477410e897a60d2cf29ca89c28ef2a2abdb81698d8ae4be2e33b934d4b", size = 13144133, upload-time = "2026-10-29T19:21:16.734Z" }, + { url = "https://files.pythonhosted.org/packages/84/ad/e4088e627b3e3cd8cbb6ad08668cde33fc7093887606de6f23e4e3e639b9/ty-0.0.95-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cef0d06e2f286db6596e004f5820d4dd3b3a1a698465840e89a5e8a5934aca7c", size = 13996368, upload-time = "2026-10-29T19:21:19.061Z" }, + { url = "https://files.pythonhosted.org/packages/26/76/67b0ee993698496e0c93b5afbec7838cc26003ac74486fc11d89c15b52fb/ty-0.0.95-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4e032018e2aaeba51a48b639acacfd949be335cd2395faa205a305a9ff524b9f", size = 14262316, upload-time = "2026-10-29T19:21:21.303Z" }, + { url = "https://files.pythonhosted.org/packages/14/23/7214c42a9d8533c5f8a6384ba6d82adda9adab2347d5fe1c395c32c4e4e9/ty-0.0.95-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:30c98a2ea2d1a731d0db27a440bceea055ba3555e3e80d694e567110ba2a477a", size = 15217155, upload-time = "2026-10-29T19:21:23.597Z" }, + { url = "https://files.pythonhosted.org/packages/60/f8/4f51df84ba70438fbf2d481baa59cc8801d86e26a472a1c0b885c01c9c99/ty-0.0.95-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c26f09e9d6039f5ffc1b35e24830da67ff6202fef8583991e2ed169c0b188a54", size = 15607522, upload-time = "2026-10-29T19:21:26.245Z" }, + { url = "https://files.pythonhosted.org/packages/bb/7d/958cfc1e9cd56390f916f36d97a00b7aa15db88b2cf1a39c22f524c6078d/ty-0.0.95-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0e01e493e87aede663405e6f2cf38c65a95456c8181ab04e9e2e9662538ea717", size = 15562445, upload-time = "2026-10-29T19:21:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/90/64/636308717989afa2fe765604ae2a76189f470d566e5e58da4482b625d64e/ty-0.0.95-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d47be2db5db80cd70c582b240a71dd8e9be215ce9288298c863c7c3611ef02b", size = 16312046, upload-time = "2026-10-29T19:21:30.801Z" }, + { url = "https://files.pythonhosted.org/packages/a9/75/e57a2c0bdce3dcf3d43ac15f3c9adc34212330e056db8a70208ab7f3730f/ty-0.0.95-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:3843233ed7ddca6d1d2e38034bf28d2618ff2fc8b16190d2efbbe46b066ea524", size = 15070529, upload-time = "2026-10-29T19:21:33.7Z" }, + { url = "https://files.pythonhosted.org/packages/a3/9f/2efd45b625fef61491986feae6f348449051ee474de4d79a77819c678aa9/ty-0.0.95-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0b24691b55fc64d480d612bbf555ff369c80d56e26c33af94acb2cbadf1a37f6", size = 13962242, upload-time = "2026-10-29T19:21:36.001Z" }, + { url = "https://files.pythonhosted.org/packages/e6/a5/d5082fbb65305af5689f1e50f3b6b8897287f1ded5d06ef713faaed17e97/ty-0.0.95-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:bf72de3cc1d528b1b19020209f4692997f402bba40d8b3e5654906edf61d1c84", size = 14265678, upload-time = "2026-10-29T19:21:38.088Z" }, + { url = "https://files.pythonhosted.org/packages/7d/e6/2347c9519c35179d7e2ca74b0ea0b9d78a503d6673905ae911dfcc13f2ed/ty-0.0.95-py3-none-musllinux_1_2_i686.whl", hash = "sha256:721260b0dc05f9bb3fe79db642a2eed94aec99de2d995c7c92e56ffd8637b1a1", size = 14636684, upload-time = "2026-10-29T19:21:40.466Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b4/bac1a90c887c0251955ed17e5da8139b4ac87352d67fcb3d1ffdb1b8723e/ty-0.0.95-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:156abe019cea9aa54e581d89ca34a48f81b8e4e9c49c203943feb60a06b822af", size = 14985737, upload-time = "2026-10-29T19:21:42.66Z" }, + { url = "https://files.pythonhosted.org/packages/68/ef/8840c75892f50107a541e7ca3fc0912d9dce133e4e13728838981901e1c1/ty-0.0.95-py3-none-win32.whl", hash = "sha256:b1903d8c17645c29ee566e708bee569d971c461f3429872305a574ccdae4c732", size = 13503067, upload-time = "2026-10-29T19:21:45.085Z" }, + { url = "https://files.pythonhosted.org/packages/b2/df/46fdcc84de81159e1d40ca103d24ac08b2450a2a51a1572333e8e796b8fc/ty-0.0.95-py3-none-win_amd64.whl", hash = "sha256:d07bd09f8a525759a595e04051e923aa54f8dc483f90e6a0b003d77a3fae0f04", size = 14097073, upload-time = "2026-10-29T19:21:47.462Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e4/84cf4f3c313448d97550fdce50e7c70b934b38765772dd4f7fdaac064cca/ty-0.0.95-py3-none-win_arm64.whl", hash = "sha256:74ac66df3cb1851e3f157c4e664aec1f28857b467c7e13f62ccf53efc6884c82", size = 13575281, upload-time = "2026-10-29T19:21:49.773Z" }, +] + +[[package]] +name = "virtualenv" +version = "21.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "packaging" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b2/24/20282bc8e69ff8dee680dc2b95e9dc81fa7e7894b14c1a8d06330e97fef7/virtualenv-21.15.1.tar.gz", hash = "sha256:e79c90b8f601701599c25905e02839924fede2c798d52325670d24fbdaab74ee", size = 5517048, upload-time = "2026-10-28T01:12:50.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/23/cdab19d3a9da42ca0938081e163e25f81e610141800ead743ba1ee881f0e/virtualenv-21.15.1-py3-none-any.whl", hash = "sha256:0d14a991689c67600316880eadc0a37b7690cb632e3b4ac67e2bc05b2b2e24a4", size = 5558848, upload-time = "2026-10-28T01:12:48.605Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.9.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/01/e5ac23733628a6bbb91913a765fcf55ee8070d8d991bc88fdaaf805052c7/wcwidth-0.9.4.tar.gz", hash = "sha256:fd8d57d3cdfe570e3265c8fab1885c24e34ef40889c0759c0c304a19e9835954", size = 970051, upload-time = "2026-10-24T18:11:44.954Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/b8/d251589a100250b71a7b8e83fedfcabfd8bd60e78cd1e3329372f63bcc93/wcwidth-0.9.4-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:b7969c9aa258d536df54c8ac8af5cad0c74a6268c9972e1fb60a5f29feffb66d", size = 612952, upload-time = "2026-10-24T18:10:55.925Z" }, + { url = "https://files.pythonhosted.org/packages/04/e9/dfdca30ca36c472de3e260406dee29fc108499ec590c63b3fe279887ee9d/wcwidth-0.9.4-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:f279f32c8e206a9092fa350e53ca0c2bd8072e45b1c9fd31bb6d476b59b591c4", size = 618904, upload-time = "2026-10-24T18:11:02.008Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/d7416f38054a107d2fb17c2b73b68723c6c334f69c4e29545fb9e6df81eb/wcwidth-0.9.4-cp310-abi3-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:a1526cd25c6420e5153bb7d764c7509e692215ee3ad9b725db1ad665d47eae09", size = 778780, upload-time = "2026-10-24T18:11:17.299Z" }, + { url = "https://files.pythonhosted.org/packages/7d/5b/42bdf9a4b0e4f9126584af2ec73642de495f3bd8add907430f13c3bb98d2/wcwidth-0.9.4-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:093469e01d881bff3db93ff3f56c364dc87c441ea01f9f41c22de0c0bdf5c6ef", size = 789012, upload-time = "2026-10-24T18:11:07.829Z" }, + { url = "https://files.pythonhosted.org/packages/fa/e8/0485850748dfdba9443f4151c7b2442cfa4987ba6cd871038eb5d6b8eca9/wcwidth-0.9.4-cp310-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b185c8b927b23af2721be1783499d1cfa5c028b098b0858951b4ae224477f1c2", size = 785946, upload-time = "2026-10-24T18:11:18.748Z" }, + { url = "https://files.pythonhosted.org/packages/e9/11/dd3d0379c6094524df6585b76ae3d9727161ff29c003177e3661c36437c1/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:17619066f95c10f44d7ae08ee456602456bc66cc9d5e91966ec73d799acebc5f", size = 795348, upload-time = "2026-10-24T18:11:09.931Z" }, + { url = "https://files.pythonhosted.org/packages/3c/48/1b5299983d9ec084be897162b7cc6014622dfe62847224fa3272b6d79ea5/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_i686.whl", hash = "sha256:b52f265a694dda6b3ad9050c8bd89158ad519109ef7c5eb110b906cc49f25763", size = 793420, upload-time = "2026-10-24T18:11:20.551Z" }, + { url = "https://files.pythonhosted.org/packages/7a/be/e3097d859b075344a7be57774a58d468eafb0ca88e02aad93eb31d69f6ba/wcwidth-0.9.4-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d8905f1390d828d745fdd1904374c90386d902d3cfa9ee88e9875099f75a4e17", size = 796192, upload-time = "2026-10-24T18:11:22.552Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/739710c8cf0de461239db5bb6ee678c9a33d30aaa22beff55cc356781818/wcwidth-0.9.4-cp310-abi3-win32.whl", hash = "sha256:4831662c396dd4c59a016ec53c0ac96738006d653a00482777d9a2e2ea323d62", size = 593229, upload-time = "2026-10-24T18:11:35.609Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f1/c123f902bc2ff9e9c2ab66241b8872204df87cbb18ecdf035e120860fedc/wcwidth-0.9.4-cp310-abi3-win_amd64.whl", hash = "sha256:f28d77153303450ee13ebbe888297e75cf532c754555b6cabafc9bdda16c421c", size = 597780, upload-time = "2026-10-24T18:11:36.952Z" }, + { url = "https://files.pythonhosted.org/packages/4b/88/1cb5ddaf5510b918e01aa95fc4b68df91dead784b63e4931037c5673e85f/wcwidth-0.9.4-cp310-abi3-win_arm64.whl", hash = "sha256:2b58fc8cf2de1bf9cdb68b8beaa5be198742414a7c8cd97aa51ec039783ffa49", size = 598786, upload-time = "2026-10-24T18:11:30.886Z" }, + { url = "https://files.pythonhosted.org/packages/b0/cc/46a31c12b4cdad04f546a3c0aca5d000d77aa5069ae88b4db24525cc29c4/wcwidth-0.9.4-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:87e4101789415a6cf55ec4c53eceeb4782c2654c88692941cb588f19dca1959f", size = 613805, upload-time = "2026-10-24T18:10:58.887Z" }, + { url = "https://files.pythonhosted.org/packages/18/f6/5a8327af9a65153ea9823d6a12fc182f325264050aa25c05a5deadc189c9/wcwidth-0.9.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ab1e5190fb82476e75fbf963e9d59b4fb1c1e1f4ced82069a0c989016aa959f7", size = 619936, upload-time = "2026-10-24T18:11:04.073Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d3/bb8a7f4aa9b915e56e09841b07636ba49c82ec987ad3d2022e1696c038d9/wcwidth-0.9.4-cp314-cp314t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:8f892713ab5795d3abae181e9c02db0eb24de4084f9ee080535369d3d98d7bb5", size = 794563, upload-time = "2026-10-24T18:11:24.4Z" }, + { url = "https://files.pythonhosted.org/packages/12/0c/986acadef534688a57502d484a40e0f30bdca2d39514fe0a311f7d575f70/wcwidth-0.9.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:befbf46467100e43fd8c8cc0c031a0105f13f5418c3be95fdcf0c47214487967", size = 806565, upload-time = "2026-10-24T18:11:14.297Z" }, + { url = "https://files.pythonhosted.org/packages/9b/1d/66a94b9ac07037d7cf2c1f1288867c9932215d467893eaa5ec8d548c7f2f/wcwidth-0.9.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c4b0dd04cca520792100f6110d10be262c94b285a281d1759c551f159214633", size = 802372, upload-time = "2026-10-24T18:11:26.275Z" }, + { url = "https://files.pythonhosted.org/packages/d1/00/1d50b8e8b45a8330b582695bf8196081a609317928a3718cf21b3d14a899/wcwidth-0.9.4-cp314-cp314t-win32.whl", hash = "sha256:14410eb73178cc492aa9bf22f7c12e3bf930e5470b7220cf6f530efbfa2e03f3", size = 598917, upload-time = "2026-10-24T18:11:38.369Z" }, + { url = "https://files.pythonhosted.org/packages/b4/fc/3e39a33fc8f5c4e07611578710275039c9d4a6655e2be7cf3459401a30cc/wcwidth-0.9.4-cp314-cp314t-win_amd64.whl", hash = "sha256:c36954849aafae1092d6f9de9548c5280c1d6841637a27ca288b2241c802c4ab", size = 603536, upload-time = "2026-10-24T18:11:39.922Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8c/b1f399fd2f8449e42978192967ab6df441d7cf894b10117983c36c062b18/wcwidth-0.9.4-cp314-cp314t-win_arm64.whl", hash = "sha256:79e09e9824c88f5e275ca99e921685567a5c5c1bb6165c16e27ddd1adab33a95", size = 605127, upload-time = "2026-10-24T18:11:32.367Z" }, + { url = "https://files.pythonhosted.org/packages/04/46/839780e772e9068580619d4d691348b7a5f9c4fa5ffe501e522eefeb27cc/wcwidth-0.9.4-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:a8c9ce14c6b4a499e61de6de7e9e456de11be8bc991554e2ab2ed4feb40b9664", size = 613850, upload-time = "2026-10-24T18:11:00.725Z" }, + { url = "https://files.pythonhosted.org/packages/d2/66/b2559c7012bbd4268f5b5c0288cb793c78235d354e306871a55893aa9d1a/wcwidth-0.9.4-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:d68f09093c9bec9226a52edc47487042ed4f64c5df91d06f00419495071c0279", size = 620207, upload-time = "2026-10-24T18:11:05.975Z" }, + { url = "https://files.pythonhosted.org/packages/d1/fe/7d2c39e28924b2430b20099e36276f14e00e75c7bbe4ba5b1ed44d1d465a/wcwidth-0.9.4-cp315-cp315t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:d76b7c715c5ca0103b28915f699f1020ae21f06f1d22cdd85a7a97f7b1536eed", size = 797056, upload-time = "2026-10-24T18:11:27.911Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d7/f4f9967a411e02b89b74df2f49f44977c4badccf29738f3ae6941619e27c/wcwidth-0.9.4-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:29c0685f5f5f8815b88308fdcbe2cd12a92996a21a5b1e18352d165174ef7a6d", size = 806169, upload-time = "2026-10-24T18:11:15.724Z" }, + { url = "https://files.pythonhosted.org/packages/ac/bb/e2b40c6522f3aa1222558487dda3551d297e07e245761b4816f6caeebc29/wcwidth-0.9.4-cp315-cp315t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e05458491759aa42b7b85f36345b3e6df082d6d2d9bf616299668fd598e0209d", size = 803462, upload-time = "2026-10-24T18:11:29.269Z" }, + { url = "https://files.pythonhosted.org/packages/a8/f2/ab69009f2dd0d3772d3e07f025fa046b33c60c195e4f2e5f000297d23547/wcwidth-0.9.4-cp315-cp315t-win32.whl", hash = "sha256:9e5090d253f5df99659cddfd3ed90dd21f6ff51989b0421331a63fb2681cbe27", size = 598931, upload-time = "2026-10-24T18:11:41.659Z" }, + { url = "https://files.pythonhosted.org/packages/63/6f/b321f10c2298e7d16cffc1a5f05f3d7e5231b45f9e68af5ee378e23bb2e5/wcwidth-0.9.4-cp315-cp315t-win_amd64.whl", hash = "sha256:12786620375dc83fabf4b2b2aa52416fef2bc6094522ea29c8a53c75b0dfe9b6", size = 603485, upload-time = "2026-10-24T18:11:43.052Z" }, + { url = "https://files.pythonhosted.org/packages/4e/cc/7eeb28059186cae72a770df79e744f232ef068192a5a588b718473e35bb4/wcwidth-0.9.4-cp315-cp315t-win_arm64.whl", hash = "sha256:2cca7b5d0b9d0ae49068d3ce806e965326a987f555b62eaa322223396e10d01b", size = 605198, upload-time = "2026-10-24T18:11:33.75Z" }, + { url = "https://files.pythonhosted.org/packages/6f/0c/309b1813b230d1dbbd6f2c2ab2bb21ef68992358977605d9ef289bbfedcc/wcwidth-0.9.4-py3-none-any.whl", hash = "sha256:901eace6d2dd25ebb917d0ce5db18e5c537e1504c905cf32f0d006c12098b05d", size = 301806, upload-time = "2026-10-24T18:10:52.651Z" }, +] + +[[package]] +name = "wrapt" +version = "2.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9c/b0/f752d19e1e83ab8bd64aeccfde32ff086aee3ef08520f7340b69a8d0a4d2/wrapt-2.5.2.tar.gz", hash = "sha256:551cb49e88b3722faad780303833bc4fa70685fb1773270fb98b40d39d9fddbf", size = 186066, upload-time = "2026-10-29T23:06:39.868Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/fe/9ccae0a147d0732397f54ff232b1a0da148a4099f17c1b156b9d3e79127e/wrapt-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bec3b6b1ae2f7ed763fb0ba1776b9b5f11283e6ff40c9cbe3a855ea652c326cf", size = 106628, upload-time = "2026-10-29T23:04:44.962Z" }, + { url = "https://files.pythonhosted.org/packages/d5/ca/a61d994df49fa422abd859e525e04b95ef2888e3818a0f451aa7d4d3527f/wrapt-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e57d79e3338b3cecee1dd80d6ef8f945f8ea7b92f0d8d5591f840ffa0afc14dc", size = 106264, upload-time = "2026-10-29T23:04:46.307Z" }, + { url = "https://files.pythonhosted.org/packages/dd/26/a6caa9a84ef5db4a273787949cea7ab51f6001d2548768af4c8e3c49debd/wrapt-2.5.2-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:092472b7b1494deef40dac5e9f6295013a9d51b112b5fbc4350f64dc2574e46a", size = 250614, upload-time = "2026-10-29T23:04:47.735Z" }, + { url = "https://files.pythonhosted.org/packages/c7/28/2189571e711ab31cb7f682c739d05f033ae0e83e12eb4d5d21473ca2cd1a/wrapt-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d108bd98ab2c5ccbf55cf2154e0a73341df930030df2e01ee35d71a44a99d3ad", size = 250770, upload-time = "2026-10-29T23:04:49.183Z" }, + { url = "https://files.pythonhosted.org/packages/80/8e/46a6cc2e8215df7fae292178a355df729547ff9b912686823201d38be269/wrapt-2.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:858511731d6ede4217960c5fecdb5ecbf90e306dfd93b837395658d61374a498", size = 230031, upload-time = "2026-10-29T23:04:50.641Z" }, + { url = "https://files.pythonhosted.org/packages/97/38/dff531d0125fe880e05fe5fcb89f144d25ffbe95aa9a2c50539eabde7567/wrapt-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b6885250b809532a634acd56e360d57c201c1cdc75cbf2b5e106241ece6cbc3", size = 247325, upload-time = "2026-10-29T23:04:52.16Z" }, + { url = "https://files.pythonhosted.org/packages/f0/5b/f4a78df4c52175d53ff095f2f5a54206a2d16e542b1dd2cdcaed901fe432/wrapt-2.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c60a5cd8b7b510056f2643d94f1c144533a568ade2891a9dd35733f5e0055777", size = 226900, upload-time = "2026-10-29T23:04:53.559Z" }, + { url = "https://files.pythonhosted.org/packages/20/6f/8eab144c717300f76f1337ba58091f4de5da0f4d772727c9f41135b325c1/wrapt-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:40e03506f09dbf4854fd69848e843dadd9b7fbbdd217837cb27b37fb8f0b5bc3", size = 239017, upload-time = "2026-10-29T23:04:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/59/1a/98e278ef2f8952a34f847a91f75899f6385762b07577c51a5ef82c768dfb/wrapt-2.5.2-cp312-cp312-win32.whl", hash = "sha256:ac00ba7d22d55d3c8d085005ce4942ff3bb118d480f889b5984f1d3454052917", size = 100660, upload-time = "2026-10-29T23:04:56.703Z" }, + { url = "https://files.pythonhosted.org/packages/7d/0a/ab645f8b4c594cac2db49bbc805bff97a9d3f2944061b865f67e0351b6c8/wrapt-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:36021ef761856bdbc10d5f3da9a95af93ef60d6acf9c72c8f849937ba2b902aa", size = 106220, upload-time = "2026-10-29T23:04:58.081Z" }, + { url = "https://files.pythonhosted.org/packages/37/1e/232868b68b925ab29ca5acbec4ad54ee42a853fcd8f596bf7e0e0e3850f1/wrapt-2.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:81bdf0bd141d9b1706a0751b2ee068c7afe6be4042712d427b32b3072d5c40b9", size = 103377, upload-time = "2026-10-29T23:04:59.366Z" }, + { url = "https://files.pythonhosted.org/packages/1b/7a/38f08a89ca8faa9b788a857f109b5e4cd1f25671a5157e2218072f64aa76/wrapt-2.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:88f982c2bb306732271aa15cded4002ce28b5d9023de9d11e927da4dcf65e441", size = 106242, upload-time = "2026-10-29T23:05:00.746Z" }, + { url = "https://files.pythonhosted.org/packages/e9/8b/2fb9b2522b7e1e7534a72fec0040bbf3d273129ec0a98316e7d37b09aa35/wrapt-2.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:269321b17467a10997cc6e14793bf1816e6ce488f9711c74b72552466d206739", size = 106366, upload-time = "2026-10-29T23:05:02.191Z" }, + { url = "https://files.pythonhosted.org/packages/c2/1e/770d73e0cc82a41602cb2cb184769c28bdbaa9b998944d577a1097c18da3/wrapt-2.5.2-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:8b039b3a296762db1908d6108365225393c4fb33c1dbc5f00aa36f8302eaeaa0", size = 248696, upload-time = "2026-10-29T23:05:03.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/2b/b4362e803d870451b5e701df5e30ded2d6fb1d9e0c01eec4dc7fa8332b57/wrapt-2.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e1af6136ac5bcfab17190a70b25d8437eb8d8f9ca2b821a0655d39275a8cb7e7", size = 248663, upload-time = "2026-10-29T23:05:05.084Z" }, + { url = "https://files.pythonhosted.org/packages/87/15/244dbd2d86285857898be5f7632e8375161953e645f250ac9778c058ab9b/wrapt-2.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d4e1cdf2efdc7181c8177f6090886e22278e70f7257df12863d2f52a9dcbbd7", size = 227568, upload-time = "2026-10-29T23:05:06.836Z" }, + { url = "https://files.pythonhosted.org/packages/62/0e/15ae5812d510fbfb65acb3a63aecbd8ffb4b944b2fd0131d968e31df89d6/wrapt-2.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e33578755e07edf3a70df3241f4cf376dad507e2cc73e737311bb00b5067f4d7", size = 245147, upload-time = "2026-10-29T23:05:08.322Z" }, + { url = "https://files.pythonhosted.org/packages/f6/c2/e611b881c05f7f2a5c88f9d89236afb091b8adba3ac89ff9040b6a837be4/wrapt-2.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:2bf23cde96e0f771a6a2c68973c1ded45c9707f8bd519ede30ecf075f3c720f6", size = 225491, upload-time = "2026-10-29T23:05:10.102Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f4/bdb253208ec907097809e34c25d199cd66686a7974257e08b57d96e3176c/wrapt-2.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eff91af619ad5d587f8855f6c02164e12d33d205c702eb73d487deb499520265", size = 237319, upload-time = "2026-10-29T23:05:11.625Z" }, + { url = "https://files.pythonhosted.org/packages/67/86/74205217834201f27bc73a76b4db67f488fd07787e12a045fb2b39f09c29/wrapt-2.5.2-cp313-cp313-win32.whl", hash = "sha256:11655df8e17658a67628cdc97fe95d6b263e023213cf18067f21e5ec0b0285f3", size = 100634, upload-time = "2026-10-29T23:05:13.073Z" }, + { url = "https://files.pythonhosted.org/packages/e0/56/7c732aed39116853d74485b4431a0ea37b05bd2bf250bdddc55ceb64f8c0/wrapt-2.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:5726faa7342ba473041bc1a276486d232b42e3927d35427729b266b1b5ecaa42", size = 106044, upload-time = "2026-10-29T23:05:14.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a7/c4f7ae891bbfef566aa63effe846f6804dac4c0461601993f4c59deaf955/wrapt-2.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:e1fb4af8aac3573770ed3e64efc39d953342b12bcb6e6479512a945eb5557523", size = 103077, upload-time = "2026-10-29T23:05:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dc/a259acf2b59eefe400b2a1c6b45bc19b0d0b583722426c9dabb276cc0d19/wrapt-2.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:229859aebfc909626ea6b5a3ae9523aba43c63fd828f876f2a813f4a5767726f", size = 106620, upload-time = "2026-10-29T23:05:17.256Z" }, + { url = "https://files.pythonhosted.org/packages/f8/c5/b34ca079b7906eb043c53c06896b4e42f3ca9efa97ac10e4e254c88e2ca6/wrapt-2.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c1ea16d7dfe7d235d29435307b1539495b4925e88b135d532892973a04569245", size = 106555, upload-time = "2026-10-29T23:05:18.656Z" }, + { url = "https://files.pythonhosted.org/packages/e2/46/f66042c9ed3626cdcdf641105f091964f6d38cd406cec96479c4a3f70640/wrapt-2.5.2-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:59b7e6d1dea7c3f38bab21954f587499c27c16ea537058eee4add6e4da1780ea", size = 251681, upload-time = "2026-10-29T23:05:20.035Z" }, + { url = "https://files.pythonhosted.org/packages/70/95/1ebe33ff8053a94b3e1a0bafe31ea94b5e9296f9c5098cbcafc4ee2bcdc9/wrapt-2.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cfd508866c421687a99bed9ce5b7b4f26fd9b93d3753685d81947ab37d54c496", size = 250919, upload-time = "2026-10-29T23:05:21.501Z" }, + { url = "https://files.pythonhosted.org/packages/1c/bf/ee90a8674c8f2179ad7aeaf8c5b875fa33a12fec7bf1ccf6cbfcc31fbbf0/wrapt-2.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:459bb66cd4bc43e35cba1f1ab2bdfb9b507e0d8dc0ec40c93a060c9cb0c5415e", size = 231773, upload-time = "2026-10-29T23:05:22.996Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ee/610ef6bf4f35e4719a5edea6bd0c4d948921d028950bc9669d1fa9234af9/wrapt-2.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a8626135ecbb0b892ca79c388581ab0ef57e087f09dc0e51a2f34185d3682055", size = 248171, upload-time = "2026-10-29T23:05:24.481Z" }, + { url = "https://files.pythonhosted.org/packages/c6/10/6f12cf42bd9a34829c902c8a617c8151a4e16e7d7cb015289eede5813f39/wrapt-2.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:7ffa0be31f651da63ee51e5b74701339feac1860b561a985c7b4869d4600e800", size = 228970, upload-time = "2026-10-29T23:05:25.904Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f1/79417514032b9015b772bf5bf006ca542937b5f6c483b9ab092f9ceff8e9/wrapt-2.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5ab31d929ca8b2b7bf9320ad2a927edfb1841609951a869a9b98723b285ffbc6", size = 239267, upload-time = "2026-10-29T23:05:27.502Z" }, + { url = "https://files.pythonhosted.org/packages/a1/ce/092f2723758586973c94afa8c38a206ce3720ec5cd3edb64ee9c8b27a1c4/wrapt-2.5.2-cp314-cp314-win32.whl", hash = "sha256:afd99530994449aaac9a58e1e268eb080bb1ffd758be43ab2f8a249ce8970c9c", size = 101091, upload-time = "2026-10-29T23:05:28.955Z" }, + { url = "https://files.pythonhosted.org/packages/23/86/201fb9180290373b7fdff18e8d600151694fdb0b10892cdee77c13eb7e6b/wrapt-2.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:fabc60720d1e4a36bbe8aaf1c32024f04c6674991313f5acaca5eeadb564c600", size = 106417, upload-time = "2026-10-29T23:05:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/73/a8/cc313dc9230f4b5029e24867a1fc1dbcb929b792b02ac5fb2e0d79df7401/wrapt-2.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:1c17ca5ce83c7145eddaa20073398ec2192dad65c8d8ff4d9b78fd2917c46a75", size = 104145, upload-time = "2026-10-29T23:05:31.788Z" }, + { url = "https://files.pythonhosted.org/packages/62/04/15e0c465088fbfdab321a672c88739f68429a18aeb81ad7995a0a5cf2065/wrapt-2.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a60863d874cc6ac917bbf44324e3f195564064dee15c84e83b19e1fbbccd3169", size = 109731, upload-time = "2026-10-29T23:05:33.131Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b6/91d6f77257afabafe19bca698b41d60dde7cf49b59b0126eb25ed6a3a90e/wrapt-2.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d41c45517912610de55546188695e2bd0fbcd70b585087cd759f85a96f47be7e", size = 110032, upload-time = "2026-10-29T23:05:34.589Z" }, + { url = "https://files.pythonhosted.org/packages/9c/84/40901e8ba08f6f18f213fea51daa41a8107ccaae4e0b6e13b842f02f5feb/wrapt-2.5.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:48fd5026977adb3e2fda292a6a796967b511935d23f97dc2ae1f127a3a8639d5", size = 284278, upload-time = "2026-10-29T23:05:36.033Z" }, + { url = "https://files.pythonhosted.org/packages/89/af/6bfdffc2688a78019762f0b14171ca47c73fae1421c3324c0850135b212c/wrapt-2.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0d7639c3287d4efcdc2c8837f1854d1ecf27aae761714ba3d95e61fe8eb48db3", size = 294060, upload-time = "2026-10-29T23:05:37.515Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/9a4e3aed00d6d877d2646072d9b6b4c9a558a8ad79a18d3abafc1079cd07/wrapt-2.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:eeaa40251c40e18c5c97ef11d5dbbad28a7b7b99fdd7a7469662799100d74dc7", size = 263272, upload-time = "2026-10-29T23:05:39.008Z" }, + { url = "https://files.pythonhosted.org/packages/5f/6f/0086e02aae9144a0fa029fb0351a8b2f91fbd28eb191c8f2ced3831cb1ba/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b9ce67ec8803e6c56e78c9ebf726f61116516212c28dfa3683d0962e15cf9a62", size = 289219, upload-time = "2026-10-29T23:05:40.459Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ac/e342f997166b7d433abc12720b6e30793fcf3a36c5457d22261558b61411/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:4d5faf5c3e435207b3a51294ed5a00e5e17e3842442ccbbd864ee9bc0efc63fb", size = 260554, upload-time = "2026-10-29T23:05:41.815Z" }, + { url = "https://files.pythonhosted.org/packages/65/64/48ddff54611f6e08ba70adf1b3dbf36c7b08b7aa5dbf7c65a03672b0eed1/wrapt-2.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2861d50a4f330fa140ddc2a9ddee33f7e4f9bdb649a23dc9be8ed6310ffa47fc", size = 278921, upload-time = "2026-10-29T23:05:43.489Z" }, + { url = "https://files.pythonhosted.org/packages/c0/bf/0ca422495491f22656b401832184a87b1e67d1dfb7f31ac8d60525c572ce/wrapt-2.5.2-cp314-cp314t-win32.whl", hash = "sha256:46cee4400437c9f33e755522be9e7ce6ebd097273f464eb6f57bee09787f0c59", size = 103586, upload-time = "2026-10-29T23:05:45.046Z" }, + { url = "https://files.pythonhosted.org/packages/69/17/922d99009a228907cd0a5f5d590bbc176dd70527d3eb06f8b4d74889edf8/wrapt-2.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1ad706a42b139ad7f03f32aed57da5e10fe8ed707e29849cfa94f1d49cba1405", size = 110111, upload-time = "2026-10-29T23:05:46.458Z" }, + { url = "https://files.pythonhosted.org/packages/68/45/d168c09643e3207c5618b33a5636137f298b9ded38b3a33fad67dc343403/wrapt-2.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:450cc93f5a2d58d4a74d384d52db4b41d5728065c0057c9bb4666c6115f2a2c9", size = 106384, upload-time = "2026-10-29T23:05:48.051Z" }, + { url = "https://files.pythonhosted.org/packages/67/8c/3b2b95cd60b78f9450d748854e421fcf48bc350d76ca9e0fe048b87ad796/wrapt-2.5.2-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:2c17ec4310cc06f864799a4c37c1c716d5dcf05ceead2e63ca992a9466903b73", size = 106621, upload-time = "2026-10-29T23:05:49.433Z" }, + { url = "https://files.pythonhosted.org/packages/16/12/c430cd1289711cdf2b6b1c469d5efa527f4815fabab32bff3249545344da/wrapt-2.5.2-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:430070776fb30d925a7a1e13d68086e4731b63eedf4c43ee62fc4c8108920cce", size = 106590, upload-time = "2026-10-29T23:05:50.967Z" }, + { url = "https://files.pythonhosted.org/packages/69/4b/d7230c57ca30023a5630bf5cc0e092ad1cfce4d999a309b8377a36b3c249/wrapt-2.5.2-cp315-cp315-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:99978d196df45c115273c6119e4d0dacbc67bc267a196506625bf7084b741f0b", size = 252192, upload-time = "2026-10-29T23:05:52.344Z" }, + { url = "https://files.pythonhosted.org/packages/fa/74/6dc841c96b9906e0c466aaeed7f51162129cc8745a91bb28168aa3b45194/wrapt-2.5.2-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:217798fb0cfb31479a264f841b0242bc1bed1ffc70d233e8444a876cec57928c", size = 251207, upload-time = "2026-10-29T23:05:54.011Z" }, + { url = "https://files.pythonhosted.org/packages/52/bb/25e710cc9d3bd25d81d46791dd25029d6f9b9d44b990e3d81755b549ff71/wrapt-2.5.2-cp315-cp315-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:62b339565cfdfcc0ba71c527b4ad13ac9de25504d474fa4ae5db95d1c7e4c6a2", size = 233048, upload-time = "2026-10-29T23:05:55.796Z" }, + { url = "https://files.pythonhosted.org/packages/4e/80/672640d799862a36abae740e778146dbe155ec8ffd6211b3a99240f599ca/wrapt-2.5.2-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:84619ca6b6f67a7dd53754ad402cafceb4ddc9d8981f144f62718b783f05786e", size = 248479, upload-time = "2026-10-29T23:05:57.346Z" }, + { url = "https://files.pythonhosted.org/packages/f6/34/91eb6724eb7cf64b77637d720ef71cad1ec6e4ce2d15eb548670c47078fa/wrapt-2.5.2-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:08d1d7bcb7dd277b7b976fd914455c3c1088f5c101ac7469ae5a5b8c03be0a0a", size = 230013, upload-time = "2026-10-29T23:05:58.745Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f8/f1fc0c3394819c9a6a3c0d0055b06614a3bef7ad133cf18f9c09a2b2141f/wrapt-2.5.2-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:4e86f3543c67d72b4d4c08c3e351523c2d69cd05932df65937d79bc472e9d62f", size = 239808, upload-time = "2026-10-29T23:06:00.266Z" }, + { url = "https://files.pythonhosted.org/packages/56/d1/70f9e71674d7b6c21c8e8cafc3016f773dc3acb31a131f23ab4af8c1effb/wrapt-2.5.2-cp315-cp315-win32.whl", hash = "sha256:035cd71e2aadfc0ad366ab4785b49e62093370885ea4b527b61b308b7a92ff14", size = 101099, upload-time = "2026-10-29T23:06:01.777Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e6/fdea9e35605a6e7fc71556068ea184fb3e9cc09d1e3be8ca8f26d16339c9/wrapt-2.5.2-cp315-cp315-win_amd64.whl", hash = "sha256:b9f41029f9bc6ba80755b9c07f5e77e9e7aafb71c625b9f67614e4ebfb24302a", size = 106423, upload-time = "2026-10-29T23:06:03.076Z" }, + { url = "https://files.pythonhosted.org/packages/d5/50/8c41d8ba868cde9afd5b68e18f19b94300d091c409ef081b80c2aecda653/wrapt-2.5.2-cp315-cp315-win_arm64.whl", hash = "sha256:ae84013e89ec1874415549a46cfea373e28ddecbb72e1f5456cddfa2b76a28e6", size = 104152, upload-time = "2026-10-29T23:06:04.356Z" }, + { url = "https://files.pythonhosted.org/packages/8a/e7/b43d341b036e2862550eb7fffe2b4130015c39c861f30eb34014a2f032d2/wrapt-2.5.2-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:b680bef5200899524667f813fa34dcfde9313fef9f9fa432985d4bc7b4195ba3", size = 109711, upload-time = "2026-10-29T23:06:05.768Z" }, + { url = "https://files.pythonhosted.org/packages/be/52/8012aaf046264e27969f79bc94cd2afb4729338f5434b90edd3d4bb60b8f/wrapt-2.5.2-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:da51f5ac290f2e024ee842799c6592f68cd880c20f6b3a54305d1b6aeab459a4", size = 110047, upload-time = "2026-10-29T23:06:07.309Z" }, + { url = "https://files.pythonhosted.org/packages/54/a7/628c315d19c6027e8b0e700c0dd7d5c3fd662329c7aa1294315064b87cdd/wrapt-2.5.2-cp315-cp315t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3ebc07822649eda442382c1d20983bc6ec5aa2bf79a600da11b29ecb62a0fb2", size = 285041, upload-time = "2026-10-29T23:06:08.777Z" }, + { url = "https://files.pythonhosted.org/packages/7a/fd/4b418c9655bacb5ce8b0aed894f8355e12940bde74237ec45d9396e1f87f/wrapt-2.5.2-cp315-cp315t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bbb6e959c1e2c32bbad291f83af6320c8c46bb64e256801e85f55d88a09522d5", size = 295329, upload-time = "2026-10-29T23:06:10.489Z" }, + { url = "https://files.pythonhosted.org/packages/92/24/dbe9776fded6dd5f2bfa1446ec04a33afe5afac1d5fb89cd7f5e96fbc61d/wrapt-2.5.2-cp315-cp315t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a0541e62f7b9c0d42130e3469d6ac3b1767f019314ae795646a2850a75c64412", size = 265088, upload-time = "2026-10-29T23:06:11.963Z" }, + { url = "https://files.pythonhosted.org/packages/9d/ee/4103576d31bed902224fed8b1d06b94313491504d1d644e42bceba4de880/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:18819c05409a161999be68da8e451636470c35f77e25c8201825a33a7d88c89c", size = 290413, upload-time = "2026-10-29T23:06:13.536Z" }, + { url = "https://files.pythonhosted.org/packages/bc/7c/944e95c25232a5805ea9f25cf2ce90386262db025ba04fbb61f451d9523f/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:2594e0be4ee09b2f19828265a39df90e591def44215fe8c5b8d6b83aa4cc18b5", size = 262383, upload-time = "2026-10-29T23:06:14.887Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b3/a4b66497e3bb63d2e94ea1605f4beaf5b6b5dafbb6ce41abd727a6fcb7b5/wrapt-2.5.2-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:32c0387872e67b9dfdd2b599bf31fe127e597ed8526c5f192cdaca57a491e9dc", size = 280180, upload-time = "2026-10-29T23:06:16.314Z" }, + { url = "https://files.pythonhosted.org/packages/91/bd/28d046dccfcade6ad62ad19f2f805e5b2e3f3463431307c3b8bd54e24e7f/wrapt-2.5.2-cp315-cp315t-win32.whl", hash = "sha256:9cb33f293e3aa04b36f7d165dc05b42db85e0a88e20a8fd81f60426d1f17d535", size = 103551, upload-time = "2026-10-29T23:06:17.732Z" }, + { url = "https://files.pythonhosted.org/packages/0d/d9/aef3035a3e3d918eb51e7db454af9e06919f0c5208fa0a9315548fef7734/wrapt-2.5.2-cp315-cp315t-win_amd64.whl", hash = "sha256:7f89423965b23aa62a90bf21c20353056a829ca9a60c01fde9aec9876c99392e", size = 110099, upload-time = "2026-10-29T23:06:19.293Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ea/56eddba6249fae5361b53eb62e2e25ad86cabec4632d943d356aba8dea1b/wrapt-2.5.2-cp315-cp315t-win_arm64.whl", hash = "sha256:fa642d3781c061579cb138773add4a55b16f99de9ebc41603e89a756cf9e62c6", size = 106397, upload-time = "2026-10-29T23:06:20.826Z" }, + { url = "https://files.pythonhosted.org/packages/77/8b/1ea57b936b70f98e8e7620a352578096d778dbee95b262f73c066d210ffb/wrapt-2.5.2-py3-none-any.whl", hash = "sha256:61eb54e9540ea522c2c4752a57549a83d3c67e6704ed708403d28e067aaf90f0", size = 82025, upload-time = "2026-10-29T23:06:38.56Z" }, +] From ff1b7ad7f919e9e6e158d959b9f313c5acd33f3e Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:25 +0000 Subject: [PATCH 02/29] feat: add the SQL lint, format and statement facts engine Built-in Rules use SQC codes. The engine exposes JSON lint, batch and format entries so host tools can embed it with their own codes. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- crates/sqlclean/Cargo.toml | 26 + crates/sqlclean/src/constants.rs | 15 + crates/sqlclean/src/lib.rs | 9 + .../src/sql_facts/_helpers/collection.rs | 213 + crates/sqlclean/src/sql_facts/_helpers/mod.rs | 3 + .../src/sql_facts/_helpers/statements.rs | 38 + .../sqlclean/src/sql_facts/_helpers/tokens.rs | 234 + .../sqlclean/src/sql_facts/main/file_facts.rs | 8 + crates/sqlclean/src/sql_facts/main/mod.rs | 1 + crates/sqlclean/src/sql_facts/mod.rs | 8 + crates/sqlclean/src/sql_facts/models.rs | 81 + crates/sqlclean/src/sql_facts/tests.rs | 6 + .../sqlclean/src/sql_facts/tests/helpers.rs | 4 + .../src/sql_facts/tests/test_file_facts.rs | 225 + .../src/sql_facts/tests/test_types.rs | 21 + .../src/sql_lint/_helpers/additional.rs | 1873 ++++++ .../sqlclean/src/sql_lint/_helpers/engine.rs | 1978 ++++++ .../src/sql_lint/_helpers/formatter.rs | 343 ++ .../src/sql_lint/_helpers/formatter_syntax.rs | 459 ++ .../src/sql_lint/_helpers/inline_relations.rs | 223 + .../src/sql_lint/_helpers/join_predicates.rs | 694 +++ crates/sqlclean/src/sql_lint/_helpers/mod.rs | 10 + .../sqlclean/src/sql_lint/_helpers/parsing.rs | 44 + .../src/sql_lint/_helpers/printed_layout.rs | 931 +++ .../src/sql_lint/_helpers/terminal_shape.rs | 303 + .../src/sql_lint/_helpers/token_layout.rs | 772 +++ crates/sqlclean/src/sql_lint/constants.rs | 61 + .../src/sql_lint/main/batch_engine.rs | 19 + .../sqlclean/src/sql_lint/main/catalogue.rs | 6 + .../src/sql_lint/main/cross_join_subject.rs | 15 + .../src/sql_lint/main/default_rules.rs | 6 + crates/sqlclean/src/sql_lint/main/engine.rs | 5 + .../src/sql_lint/main/fill_finding_path.rs | 9 + .../src/sql_lint/main/format_requests.rs | 8 + .../sqlclean/src/sql_lint/main/formatter.rs | 5 + crates/sqlclean/src/sql_lint/main/lint.rs | 8 + crates/sqlclean/src/sql_lint/main/mod.rs | 11 + .../sqlclean/src/sql_lint/main/parse_sql.rs | 8 + .../src/sql_lint/main/validate_dialect.rs | 13 + crates/sqlclean/src/sql_lint/mod.rs | 9 + crates/sqlclean/src/sql_lint/models.rs | 188 + crates/sqlclean/src/sql_lint/tests.rs | 34 + .../tests/fixtures/FORMAT_CORPUS_NOTICE.md | 32 + .../tests/fixtures/format_corpus.json | 5354 +++++++++++++++++ crates/sqlclean/src/sql_lint/tests/helpers.rs | 304 + .../sql_lint/tests/test_authored_tokens.rs | 416 ++ .../src/sql_lint/tests/test_batch_lint.rs | 43 + .../sql_lint/tests/test_comment_formatter.rs | 95 + .../src/sql_lint/tests/test_cross_joins.rs | 286 + .../sql_lint/tests/test_empty_fixture_lint.rs | 61 + .../src/sql_lint/tests/test_format_corpus.rs | 61 + .../tests/test_function_call_spacing.rs | 117 + .../tests/test_idempotency_reprint.rs | 48 + .../src/sql_lint/tests/test_layout_rules.rs | 287 + .../src/sql_lint/tests/test_line_wrap.rs | 107 + .../sql_lint/tests/test_lint_exemptions.rs | 130 + .../tests/test_set_operation_formatter.rs | 59 + .../src/sql_lint/tests/test_sql_lint.rs | 1969 ++++++ .../sqlclean/src/sql_lint/tests/test_types.rs | 182 + .../sql_lint/tests/test_unused_table_alias.rs | 317 + .../sql_lint/tests/test_values_formatter.rs | 53 + .../src/sql_quality/_helpers/cte_columns.rs | 457 ++ .../src/sql_quality/_helpers/declared_keys.rs | 50 + .../src/sql_quality/_helpers/json_parses.rs | 201 + .../sqlclean/src/sql_quality/_helpers/keys.rs | 437 ++ .../src/sql_quality/_helpers/literals.rs | 71 + .../src/sql_quality/_helpers/ranking.rs | 218 + .../sql_quality/_helpers/removal_safety.rs | 102 + .../src/sql_quality/_helpers/setting_help.rs | 26 + .../src/sql_quality/_helpers/syntax.rs | 124 + crates/sqlclean/src/sql_quality/constants.rs | 202 + .../main/declared_relation_keys.rs | 38 + .../src/sql_quality/main/diagnostics.rs | 32 + crates/sqlclean/src/sql_quality/main/mod.rs | 2 + crates/sqlclean/src/sql_quality/mod.rs | 27 + crates/sqlclean/src/sql_quality/models.rs | 47 + crates/sqlclean/src/sql_quality/tests.rs | 10 + .../sqlclean/src/sql_quality/tests/helpers.rs | 116 + .../src/sql_quality/tests/test_json_parses.rs | 282 + .../sql_quality/tests/test_quality_rules.rs | 403 ++ .../sql_quality/tests/test_quality_scale.rs | 43 + .../src/sql_quality/tests/test_types.rs | 38 + crates/sqlclean/src/sql_quality/types.rs | 6 + .../sql_tokens/_helpers/builtin_functions.rs | 151 + .../sqlclean/src/sql_tokens/_helpers/mod.rs | 3 + .../sql_tokens/_helpers/reserved_keywords.rs | 43 + .../src/sql_tokens/_helpers/token_context.rs | 30 + crates/sqlclean/src/sql_tokens/constants.rs | 35 + .../sql_tokens/main/builtin_function_names.rs | 6 + .../src/sql_tokens/main/canonical_tokens.rs | 25 + .../src/sql_tokens/main/case_folding.rs | 23 + .../src/sql_tokens/main/is_unquoted_word.rs | 11 + crates/sqlclean/src/sql_tokens/main/mod.rs | 10 + .../src/sql_tokens/main/name_positions.rs | 16 + .../src/sql_tokens/main/projection_spans.rs | 41 + .../src/sql_tokens/main/query_fingerprint.rs | 25 + .../src/sql_tokens/main/token_texts.rs | 16 + crates/sqlclean/src/sql_tokens/mod.rs | 9 + crates/sqlclean/src/sql_tokens/models.rs | 7 + .../sql_tokens/reserved_keywords/bigquery.txt | 99 + .../reserved_keywords/databricks.txt | 4 + .../sql_tokens/reserved_keywords/duckdb.txt | 77 + .../sql_tokens/reserved_keywords/generic.txt | 28 + .../sql_tokens/reserved_keywords/postgres.txt | 80 + .../reserved_keywords/snowflake.txt | 65 + .../src/sql_tokens/reserved_keywords/tsql.txt | 186 + crates/sqlclean/src/sql_tokens/tests.rs | 4 + .../sql_tokens/tests/test_canonical_tokens.rs | 218 + .../src/sql_tokens/tests/test_types.rs | 7 + 109 files changed, 22999 insertions(+) create mode 100644 crates/sqlclean/Cargo.toml create mode 100644 crates/sqlclean/src/constants.rs create mode 100644 crates/sqlclean/src/lib.rs create mode 100644 crates/sqlclean/src/sql_facts/_helpers/collection.rs create mode 100644 crates/sqlclean/src/sql_facts/_helpers/mod.rs create mode 100644 crates/sqlclean/src/sql_facts/_helpers/statements.rs create mode 100644 crates/sqlclean/src/sql_facts/_helpers/tokens.rs create mode 100644 crates/sqlclean/src/sql_facts/main/file_facts.rs create mode 100644 crates/sqlclean/src/sql_facts/main/mod.rs create mode 100644 crates/sqlclean/src/sql_facts/mod.rs create mode 100644 crates/sqlclean/src/sql_facts/models.rs create mode 100644 crates/sqlclean/src/sql_facts/tests.rs create mode 100644 crates/sqlclean/src/sql_facts/tests/helpers.rs create mode 100644 crates/sqlclean/src/sql_facts/tests/test_file_facts.rs create mode 100644 crates/sqlclean/src/sql_facts/tests/test_types.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/additional.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/engine.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/formatter.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/formatter_syntax.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/inline_relations.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/join_predicates.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/mod.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/parsing.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/terminal_shape.rs create mode 100644 crates/sqlclean/src/sql_lint/_helpers/token_layout.rs create mode 100644 crates/sqlclean/src/sql_lint/constants.rs create mode 100644 crates/sqlclean/src/sql_lint/main/batch_engine.rs create mode 100644 crates/sqlclean/src/sql_lint/main/catalogue.rs create mode 100644 crates/sqlclean/src/sql_lint/main/cross_join_subject.rs create mode 100644 crates/sqlclean/src/sql_lint/main/default_rules.rs create mode 100644 crates/sqlclean/src/sql_lint/main/engine.rs create mode 100644 crates/sqlclean/src/sql_lint/main/fill_finding_path.rs create mode 100644 crates/sqlclean/src/sql_lint/main/format_requests.rs create mode 100644 crates/sqlclean/src/sql_lint/main/formatter.rs create mode 100644 crates/sqlclean/src/sql_lint/main/lint.rs create mode 100644 crates/sqlclean/src/sql_lint/main/mod.rs create mode 100644 crates/sqlclean/src/sql_lint/main/parse_sql.rs create mode 100644 crates/sqlclean/src/sql_lint/main/validate_dialect.rs create mode 100644 crates/sqlclean/src/sql_lint/mod.rs create mode 100644 crates/sqlclean/src/sql_lint/models.rs create mode 100644 crates/sqlclean/src/sql_lint/tests.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/fixtures/FORMAT_CORPUS_NOTICE.md create mode 100644 crates/sqlclean/src/sql_lint/tests/fixtures/format_corpus.json create mode 100644 crates/sqlclean/src/sql_lint/tests/helpers.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_authored_tokens.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_batch_lint.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_comment_formatter.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_cross_joins.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_empty_fixture_lint.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_function_call_spacing.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_idempotency_reprint.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_lint_exemptions.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_set_operation_formatter.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_sql_lint.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_types.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_unused_table_alias.rs create mode 100644 crates/sqlclean/src/sql_lint/tests/test_values_formatter.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/cte_columns.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/declared_keys.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/json_parses.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/keys.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/literals.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/ranking.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/removal_safety.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/setting_help.rs create mode 100644 crates/sqlclean/src/sql_quality/_helpers/syntax.rs create mode 100644 crates/sqlclean/src/sql_quality/constants.rs create mode 100644 crates/sqlclean/src/sql_quality/main/declared_relation_keys.rs create mode 100644 crates/sqlclean/src/sql_quality/main/diagnostics.rs create mode 100644 crates/sqlclean/src/sql_quality/main/mod.rs create mode 100644 crates/sqlclean/src/sql_quality/mod.rs create mode 100644 crates/sqlclean/src/sql_quality/models.rs create mode 100644 crates/sqlclean/src/sql_quality/tests.rs create mode 100644 crates/sqlclean/src/sql_quality/tests/helpers.rs create mode 100644 crates/sqlclean/src/sql_quality/tests/test_json_parses.rs create mode 100644 crates/sqlclean/src/sql_quality/tests/test_quality_rules.rs create mode 100644 crates/sqlclean/src/sql_quality/tests/test_quality_scale.rs create mode 100644 crates/sqlclean/src/sql_quality/tests/test_types.rs create mode 100644 crates/sqlclean/src/sql_quality/types.rs create mode 100644 crates/sqlclean/src/sql_tokens/_helpers/builtin_functions.rs create mode 100644 crates/sqlclean/src/sql_tokens/_helpers/mod.rs create mode 100644 crates/sqlclean/src/sql_tokens/_helpers/reserved_keywords.rs create mode 100644 crates/sqlclean/src/sql_tokens/_helpers/token_context.rs create mode 100644 crates/sqlclean/src/sql_tokens/constants.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/builtin_function_names.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/canonical_tokens.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/case_folding.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/is_unquoted_word.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/mod.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/name_positions.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/projection_spans.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/query_fingerprint.rs create mode 100644 crates/sqlclean/src/sql_tokens/main/token_texts.rs create mode 100644 crates/sqlclean/src/sql_tokens/mod.rs create mode 100644 crates/sqlclean/src/sql_tokens/models.rs create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/bigquery.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/databricks.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/duckdb.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/generic.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/postgres.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/snowflake.txt create mode 100644 crates/sqlclean/src/sql_tokens/reserved_keywords/tsql.txt create mode 100644 crates/sqlclean/src/sql_tokens/tests.rs create mode 100644 crates/sqlclean/src/sql_tokens/tests/test_canonical_tokens.rs create mode 100644 crates/sqlclean/src/sql_tokens/tests/test_types.rs diff --git a/crates/sqlclean/Cargo.toml b/crates/sqlclean/Cargo.toml new file mode 100644 index 0000000..f9dc9b9 --- /dev/null +++ b/crates/sqlclean/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "sqlclean" +version.workspace = true +description = "SQL lint Rules, SQL quality Rules and deterministic formatting over Polyglot." +edition.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +rust-version.workspace = true + +[features] +default = [] +# Expose test-only helpers to the tests of dependent crates. +test-support = [] + +[dependencies] +polyglot-sql.workspace = true +polyglot-sql-function-catalogs.workspace = true +rayon.workspace = true +rustc-hash.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true + +[lints] +workspace = true diff --git a/crates/sqlclean/src/constants.rs b/crates/sqlclean/src/constants.rs new file mode 100644 index 0000000..2954b0f --- /dev/null +++ b/crates/sqlclean/src/constants.rs @@ -0,0 +1,15 @@ +//! Constants shared by the engine modules, and the placeholders hosts write for template code. + +pub(crate) const SQL_WILDCARD: &str = "*"; +pub(crate) const OPEN_PAREN: &str = "("; +pub(crate) const CLOSE_PAREN: &str = ")"; +pub(crate) const EMPTY_FIXTURE_QUERY_TOKEN_COUNT: usize = 6; +pub(crate) const ENCLOSING_CTE_TOKEN_COUNT: usize = 3; +/// The indent of each line of a configuration snippet printed in remediation text. +pub(crate) const SETTING_SNIPPET_INDENT: &str = " "; +/// The configuration file remediation text names when the request does not name one. +pub const DEFAULT_CONFIG_FILE: &str = "sqlclean.toml"; +/// The identifier prefix a host writes in place of each template interpolation. +pub const LINT_SENTINEL_PREFIX: &str = "__sqb_lint_"; +/// The identifier prefix a host writes in place of a SQL macro parameter. +pub const SQL_MACRO_PARAMETER_SENTINEL_PREFIX: &str = "__sqlbuild_macro_parameter_"; diff --git a/crates/sqlclean/src/lib.rs b/crates/sqlclean/src/lib.rs new file mode 100644 index 0000000..78fd494 --- /dev/null +++ b/crates/sqlclean/src/lib.rs @@ -0,0 +1,9 @@ +//! SQLClean's engine: lint and quality Rules, statement facts and the formatter, over Polyglot. + +#![forbid(unsafe_code)] + +pub mod constants; +pub mod sql_facts; +pub mod sql_lint; +pub mod sql_quality; +pub mod sql_tokens; diff --git a/crates/sqlclean/src/sql_facts/_helpers/collection.rs b/crates/sqlclean/src/sql_facts/_helpers/collection.rs new file mode 100644 index 0000000..ad5efc3 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/_helpers/collection.rs @@ -0,0 +1,213 @@ +//! Walk each parsed statement once and collect its structural facts. + +use polyglot_sql::expressions::{Column, Cte, Join, TableRef, With}; +use polyglot_sql::tokens::{Span, Token, TokenType}; +use polyglot_sql::{Expression, ExpressionWalk}; + +use crate::sql_facts::_helpers::statements::{StatementTokens, statement_token_ranges}; +use crate::sql_facts::_helpers::tokens::{ + function_nodes, join_clause_span, literal_nodes, star_nodes, +}; +use crate::sql_facts::models::{ + ColumnNode, FileFacts, JoinNode, NamedNode, StatementFacts, TableNode, +}; +use crate::sql_lint::main::parse_sql::parse_sql; +use crate::sql_lint::models::ParsedSql; + +pub(crate) fn file_facts(sql: &str, dialect: &str) -> Result { + let ParsedSql { + tokens, statements, .. + } = parse_sql(sql, dialect)?; + let characters: Vec = sql.chars().collect(); + let ranges: Vec = statement_token_ranges(&tokens); + if ranges.len() != statements.len() { + return Err(format!( + "found {} statements but {} statement ranges", + statements.len(), + ranges.len() + )); + } + let facts: Vec = statements + .iter() + .zip(ranges) + .enumerate() + .map(|(index, (statement, range))| { + statement_facts( + &StatementSource { + index, + statement, + tokens: &tokens[range.first..=range.last], + terminated_end: range.terminated_end, + }, + &characters, + ) + }) + .collect(); + Ok(FileFacts { statements: facts }) +} + +/// One parsed statement, its tokens and the end offset of its terminator. +struct StatementSource<'a> { + index: usize, + statement: &'a Expression, + tokens: &'a [Token], + terminated_end: usize, +} + +fn statement_facts(source: &StatementSource<'_>, characters: &[char]) -> StatementFacts { + let StatementSource { + index, + statement, + tokens, + terminated_end, + } = *source; + let nodes: Vec<&Expression> = statement_nodes(statement); + StatementFacts { + index, + kind: statement.variant_name().to_owned(), + start: tokens.first().map_or(0, |token| token.span.start), + end: tokens.last().map_or(0, |token| token.span.end), + terminated_end, + ctes: sorted( + nodes + .iter() + .flat_map(|node| with_ctes(node)) + .filter_map(|cte| cte_node(cte, tokens)) + .collect(), + ), + tables: sorted(nodes.iter().filter_map(|node| table_node(node)).collect()), + joins: sorted( + nodes + .iter() + .flat_map(|node| select_joins(node)) + .filter_map(|join| join_node(join, tokens)) + .collect(), + ), + stars: star_nodes(tokens), + functions: function_nodes(tokens), + columns: sorted(nodes.iter().filter_map(|node| column_node(node)).collect()), + literals: literal_nodes(tokens, characters), + } +} + +/// Every node of `expression`, including the bodies of the CTEs its queries define. +fn statement_nodes(expression: &Expression) -> Vec<&Expression> { + let mut nodes: Vec<&Expression> = Vec::new(); + for node in expression.dfs() { + nodes.push(node); + for cte in with_ctes(node) { + nodes.extend(statement_nodes(&cte.this)); + } + } + nodes +} + +fn sorted(mut items: Vec) -> Vec { + items.sort(); + items.dedup(); + items +} + +fn with_ctes(node: &Expression) -> &[Cte] { + let with: Option<&With> = match node { + Expression::Select(select) => select.with.as_ref(), + Expression::Union(union) => union.with.as_ref(), + Expression::Intersect(intersect) => intersect.with.as_ref(), + Expression::Except(except) => except.with.as_ref(), + Expression::Insert(insert) => insert.with.as_ref(), + Expression::Update(update) => update.with.as_ref(), + Expression::Delete(delete) => delete.with.as_ref(), + _ => None, + }; + with.map_or(&[], |with| with.ctes.as_slice()) +} + +fn select_joins(node: &Expression) -> &[Join] { + match node { + Expression::Select(select) => select.joins.as_slice(), + _ => &[], + } +} + +/// The CTE located at its name; the parser leaves CTE names unlocated, so read `name AS (`. +fn cte_node(cte: &Cte, tokens: &[Token]) -> Option { + let span: Span = cte.alias.span.or_else(|| { + tokens + .windows(2) + .find(|pair| { + pair[1].token_type == TokenType::As + && pair[0] + .text + .trim_matches(['"', '`', '[', ']']) + .eq_ignore_ascii_case(&cte.alias.name) + }) + .map(|pair| pair[0].span) + })?; + Some(NamedNode { + name: cte.alias.name.clone(), + start: span.start, + end: span.end, + }) +} + +fn table_node(node: &Expression) -> Option { + let Expression::Table(table) = node else { + return None; + }; + let span: Span = table.span.or(table.name.span)?; + let qualified_name: String = [&table.catalog, &table.schema] + .into_iter() + .flatten() + .map(|part| part.name.as_str()) + .chain([table.name.name.as_str()]) + .collect::>() + .join("."); + Some(TableNode { + start: span.start, + end: span.end, + name: table.name.name.clone(), + qualified_name, + alias: table.alias.as_ref().map(|alias| alias.name.clone()), + }) +} + +fn join_node(join: &Join, tokens: &[Token]) -> Option { + let relation: Option<&TableRef> = match &join.this { + Expression::Table(table) => Some(table), + _ => None, + }; + let (start, end) = join_clause_span(tokens, first_span(&join.this)?.start)?; + Some(JoinNode { + start, + end, + kind: format!("{:?}", join.kind).to_ascii_uppercase(), + relation: relation.map(|table| table.name.name.clone()), + has_condition: join.on.is_some() || !join.using.is_empty(), + }) +} + +fn column_node(node: &Expression) -> Option { + let Expression::Column(column) = node else { + return None; + }; + let column: &Column = column; + let span: Span = column.span.or(column.name.span)?; + Some(ColumnNode { + start: span.start, + end: span.end, + name: column.name.name.clone(), + qualifier: column.table.as_ref().map(|table| table.name.clone()), + }) +} + +/// The first located table or column inside `expression`, in source order. +fn first_span(expression: &Expression) -> Option { + expression + .dfs() + .filter_map(|node| match node { + Expression::Table(table) => table.span.or(table.name.span), + Expression::Column(column) => column.span, + _ => None, + }) + .min_by_key(|span| span.start) +} diff --git a/crates/sqlclean/src/sql_facts/_helpers/mod.rs b/crates/sqlclean/src/sql_facts/_helpers/mod.rs new file mode 100644 index 0000000..b7d4c25 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod collection; +pub mod statements; +pub mod tokens; diff --git a/crates/sqlclean/src/sql_facts/_helpers/statements.rs b/crates/sqlclean/src/sql_facts/_helpers/statements.rs new file mode 100644 index 0000000..fbc66d8 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/_helpers/statements.rs @@ -0,0 +1,38 @@ +//! Statement source ranges from the token stream. + +use polyglot_sql::tokens::{Token, TokenType}; + +/// The first and last token index of one statement, and the end of its terminator. +pub(crate) struct StatementTokens { + pub(crate) first: usize, + pub(crate) last: usize, + pub(crate) terminated_end: usize, +} + +/// The tokens of each non-empty statement, split at semicolons. +pub(crate) fn statement_token_ranges(tokens: &[Token]) -> Vec { + let mut ranges: Vec = Vec::new(); + let mut first: Option = None; + for (index, token) in tokens.iter().enumerate() { + if token.token_type == TokenType::Semicolon { + if let Some(start) = first.take() { + ranges.push(StatementTokens { + first: start, + last: index - 1, + terminated_end: token.span.end, + }); + } + } else if first.is_none() { + first = Some(index); + } + } + if let Some(start) = first { + let last: usize = tokens.len() - 1; + ranges.push(StatementTokens { + first: start, + last, + terminated_end: tokens[last].span.end, + }); + } + ranges +} diff --git a/crates/sqlclean/src/sql_facts/_helpers/tokens.rs b/crates/sqlclean/src/sql_facts/_helpers/tokens.rs new file mode 100644 index 0000000..933a1c1 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/_helpers/tokens.rs @@ -0,0 +1,234 @@ +//! Function calls and literals, which the syntax tree does not locate, read from tokens. + +use polyglot_sql::tokens::{Token, TokenType}; + +use crate::sql_facts::models::{LiteralNode, NamedNode, StarNode}; + +const STRING_KIND: &str = "string"; +const NUMBER_KIND: &str = "number"; +/// Words before `name (` that make the parentheses a column list rather than a call. +const COLUMN_LIST_OWNERS: [&str; 4] = ["INTO", "TABLE", "VIEW", "INDEX"]; + +/// Every call of a named function in `tokens`, located at its name. +pub(crate) fn function_nodes(tokens: &[Token]) -> Vec { + tokens + .windows(2) + .enumerate() + .filter(|(index, pair)| { + pair[1].token_type == TokenType::LParen + && is_callable_name(&pair[0]) + && !owns_column_list(tokens, *index) + }) + .map(|(_, pair)| NamedNode { + name: pair[0].text.clone(), + start: pair[0].span.start, + end: pair[0].span.end, + }) + .collect() +} + +/// Every string and numeric literal in `tokens`. +pub(crate) fn literal_nodes(tokens: &[Token], sql: &[char]) -> Vec { + tokens + .iter() + .filter_map(|token| { + let kind: &str = match token.token_type { + TokenType::String | TokenType::NationalString => STRING_KIND, + TokenType::Number => NUMBER_KIND, + _ => return None, + }; + Some(LiteralNode { + kind: kind.to_owned(), + text: sql + .get(token.span.start..token.span.end) + .map_or_else(|| token.text.clone(), |chars| chars.iter().collect()), + start: token.span.start, + end: token.span.end, + }) + }) + .collect() +} + +/// Every `*` or `qualifier.*` projection in `tokens`; the parser leaves stars unlocated. +pub(crate) fn star_nodes(tokens: &[Token]) -> Vec { + tokens + .iter() + .enumerate() + .filter(|(_, token)| token.token_type == TokenType::Star) + .filter_map(|(index, star)| { + let previous: &Token = tokens.get(index.checked_sub(1)?)?; + if previous.token_type == TokenType::Dot { + let qualifier: &Token = tokens.get(index.checked_sub(2)?)?; + return Some(StarNode { + qualifier: Some(qualifier.text.clone()), + start: qualifier.span.start, + end: star.span.end, + }); + } + matches!( + previous.token_type, + TokenType::Select | TokenType::Comma | TokenType::Distinct | TokenType::All + ) + .then(|| StarNode { + qualifier: None, + start: star.span.start, + end: star.span.end, + }) + }) + .collect() +} + +fn is_callable_name(token: &Token) -> bool { + let adjacent_word: bool = token + .text + .chars() + .next() + .is_some_and(|first| first.is_alphabetic() || first == '_'); + adjacent_word + && !matches!( + token.token_type, + TokenType::String | TokenType::Number | TokenType::NationalString + ) + && !is_clause_keyword(token) +} + +fn is_clause_keyword(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::In + | TokenType::Exists + | TokenType::As + | TokenType::Values + | TokenType::Over + | TokenType::Using + | TokenType::On + | TokenType::From + | TokenType::Join + | TokenType::With + | TokenType::Where + | TokenType::And + | TokenType::Or + | TokenType::Not + | TokenType::Select + | TokenType::Union + | TokenType::Window + | TokenType::Lateral + ) +} + +fn owns_column_list(tokens: &[Token], name_index: usize) -> bool { + name_index + .checked_sub(1) + .and_then(|previous| tokens.get(previous)) + .is_some_and(|previous| { + COLUMN_LIST_OWNERS + .iter() + .any(|owner| previous.text.eq_ignore_ascii_case(owner)) + }) +} + +/// The character span of the join clause whose relation starts at `relation_start`. +pub(crate) fn join_clause_span(tokens: &[Token], relation_start: usize) -> Option<(usize, usize)> { + let relation: usize = tokens + .iter() + .position(|token| token.span.start >= relation_start)?; + let start: usize = join_clause_start(tokens, relation); + let end: usize = join_clause_end(tokens, start); + Some((tokens[start].span.start, tokens[end].span.end)) +} + +/// The first token of the clause: its join keyword and modifiers, or the relation of a comma join. +fn join_clause_start(tokens: &[Token], relation: usize) -> usize { + let keyword: Option = tokens[..relation] + .iter() + .rposition(|token| { + matches!( + token.token_type, + TokenType::Join | TokenType::StraightJoin | TokenType::Comma | TokenType::From + ) + }) + .filter(|index| { + matches!( + tokens[*index].token_type, + TokenType::Join | TokenType::StraightJoin + ) + }); + let Some(mut start) = keyword else { + return relation; + }; + while start > 0 && is_join_modifier(&tokens[start - 1]) { + start -= 1; + } + start +} + +/// The last token of the clause that begins at `start`, before the next clause at its depth. +fn join_clause_end(tokens: &[Token], start: usize) -> usize { + let keywords: usize = tokens[start..] + .iter() + .take_while(|token| { + is_join_modifier(token) + || matches!(token.token_type, TokenType::Join | TokenType::StraightJoin) + }) + .count(); + let mut depth: usize = 0; + for index in start + keywords.max(1)..tokens.len() { + let token: &Token = &tokens[index]; + match token.token_type { + TokenType::LParen => depth += 1, + TokenType::RParen if depth == 0 => return index - 1, + TokenType::RParen => depth -= 1, + _ if depth == 0 && ends_join_clause(tokens, index) => return index - 1, + _ => {} + } + } + tokens.len() - 1 +} + +fn is_join_modifier(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::Left + | TokenType::Right + | TokenType::Inner + | TokenType::Outer + | TokenType::Full + | TokenType::Cross + | TokenType::Natural + | TokenType::Semi + | TokenType::Anti + | TokenType::Positional + ) +} + +/// The token at `index` begins another join or clause, rather than continuing this one. +fn ends_join_clause(tokens: &[Token], index: usize) -> bool { + let token: &Token = &tokens[index]; + let calls_function: bool = tokens + .get(index + 1) + .is_some_and(|next| next.token_type == TokenType::LParen); + let starts_join: bool = (is_join_modifier(token) && !calls_function) + || matches!(token.token_type, TokenType::Join | TokenType::StraightJoin); + starts_join + || matches!( + token.token_type, + TokenType::Comma + | TokenType::Semicolon + | TokenType::Where + | TokenType::Group + | TokenType::GroupBy + | TokenType::Order + | TokenType::OrderBy + | TokenType::Having + | TokenType::Limit + | TokenType::Offset + | TokenType::Fetch + | TokenType::Qualify + | TokenType::Window + | TokenType::Union + | TokenType::Except + | TokenType::Intersect + | TokenType::Pivot + | TokenType::Unpivot + ) +} diff --git a/crates/sqlclean/src/sql_facts/main/file_facts.rs b/crates/sqlclean/src/sql_facts/main/file_facts.rs new file mode 100644 index 0000000..31bae43 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/main/file_facts.rs @@ -0,0 +1,8 @@ +//! The structural facts of every statement in one SQL file. + +use crate::sql_facts::models::FileFacts; + +/// Parse `sql` under `dialect` and return each statement's structural facts. +pub fn file_facts(sql: &str, dialect: &str) -> Result { + crate::sql_facts::_helpers::collection::file_facts(sql, dialect) +} diff --git a/crates/sqlclean/src/sql_facts/main/mod.rs b/crates/sqlclean/src/sql_facts/main/mod.rs new file mode 100644 index 0000000..2177dca --- /dev/null +++ b/crates/sqlclean/src/sql_facts/main/mod.rs @@ -0,0 +1 @@ +pub mod file_facts; diff --git a/crates/sqlclean/src/sql_facts/mod.rs b/crates/sqlclean/src/sql_facts/mod.rs new file mode 100644 index 0000000..1d1f678 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/mod.rs @@ -0,0 +1,8 @@ +//! Structural facts about each SQL statement, which custom Python Rules read. + +pub mod _helpers; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean/src/sql_facts/models.rs b/crates/sqlclean/src/sql_facts/models.rs new file mode 100644 index 0000000..bd8e28e --- /dev/null +++ b/crates/sqlclean/src/sql_facts/models.rs @@ -0,0 +1,81 @@ +//! Statement facts, located by Unicode character offsets as Python string indexes are. + +use serde::Serialize; + +/// Every statement of one file, in source order. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct FileFacts { + pub statements: Vec, +} + +/// One statement's source range, kind and the common structures it contains. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct StatementFacts { + pub index: usize, + pub kind: String, + pub start: usize, + pub end: usize, + /// The end of the statement including its terminating semicolon, when it has one. + pub terminated_end: usize, + pub ctes: Vec, + pub tables: Vec, + pub joins: Vec, + pub stars: Vec, + pub functions: Vec, + pub columns: Vec, + pub literals: Vec, +} + +/// A named structure such as a CTE or a function call, located at its name. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct NamedNode { + pub start: usize, + pub end: usize, + pub name: String, +} + +/// A relation read in FROM or JOIN, located at its name. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct TableNode { + pub start: usize, + pub end: usize, + pub name: String, + pub qualified_name: String, + pub alias: Option, +} + +/// One join, located at the relation it joins. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct JoinNode { + pub start: usize, + pub end: usize, + pub kind: String, + pub relation: Option, + pub has_condition: bool, +} + +/// One `*` or `qualifier.*` projection. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct StarNode { + pub start: usize, + pub end: usize, + pub qualifier: Option, +} + +/// One column reference, with its qualifier when written. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct ColumnNode { + pub start: usize, + pub end: usize, + pub name: String, + pub qualifier: Option, +} + +/// One string or numeric literal as written. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize)] +pub struct LiteralNode { + pub start: usize, + pub end: usize, + pub kind: String, + pub text: String, +} diff --git a/crates/sqlclean/src/sql_facts/tests.rs b/crates/sqlclean/src/sql_facts/tests.rs new file mode 100644 index 0000000..5032545 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/tests.rs @@ -0,0 +1,6 @@ +#[path = "tests/test_file_facts.rs"] +mod file_facts; +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean/src/sql_facts/tests/helpers.rs b/crates/sqlclean/src/sql_facts/tests/helpers.rs new file mode 100644 index 0000000..19d7237 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/tests/helpers.rs @@ -0,0 +1,4 @@ +/// The characters of `sql` from `start` to `end`. +pub(crate) fn text(sql: &str, start: usize, end: usize) -> String { + sql.chars().skip(start).take(end - start).collect() +} diff --git a/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs b/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs new file mode 100644 index 0000000..0a275c9 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs @@ -0,0 +1,225 @@ +use crate::sql_facts::main::file_facts::file_facts; +use crate::sql_facts::models::{FileFacts, StatementFacts}; +use crate::sql_facts::tests::helpers::text; +use crate::sql_facts::tests::test_types::{ParseOutcomeTestCase, StatementFactsTestCase}; + +const TWO_STATEMENTS: &str = "WITH recent AS (SELECT o.order_id, o.* FROM shop.orders AS o WHERE o.total > 10)\n\ +SELECT r.order_id, UPPER(c.name) AS name, 'é' AS mark\n\ +FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id;\n\ +SELECT COUNT(*) FROM customers"; + +#[test] +fn given_statements_when_collecting_facts_then_every_structure_is_located_in_characters() +-> Result<(), String> { + let test_cases = [ + StatementFactsTestCase { + description: "a CTE query after a multibyte literal locates every node by character", + sql: TWO_STATEMENTS, + statement: 0, + expected_kind: "select", + expected_text: "WITH recent AS (SELECT o.order_id, o.* FROM shop.orders AS o WHERE o.total > 10)\n\ +SELECT r.order_id, UPPER(c.name) AS name, 'é' AS mark\n\ +FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", + expected_terminator: ";", + expected_ctes: &["recent"], + expected_tables: &[("shop.orders", "o"), ("recent", "r"), ("customers", "c")], + expected_joins: &[( + "LEFT", + "customers", + true, + "LEFT JOIN customers AS c ON r.customer_id = c.customer_id", + )], + expected_stars: &["o.*"], + expected_functions: &["UPPER"], + expected_literals: &["10", "'é'"], + expected_columns: &[("c", "customer_id"), ("o", "total")], + }, + StatementFactsTestCase { + description: "the last statement has no terminator and spans its own text", + sql: TWO_STATEMENTS, + statement: 1, + expected_kind: "select", + expected_text: "SELECT COUNT(*) FROM customers", + expected_terminator: "", + expected_ctes: &[], + expected_tables: &[("customers", "")], + expected_joins: &[], + expected_stars: &[], + expected_functions: &["COUNT"], + expected_literals: &[], + expected_columns: &[], + }, + StatementFactsTestCase { + description: "a join without a condition reports it", + sql: "SELECT a.id FROM a CROSS JOIN b", + statement: 0, + expected_kind: "select", + expected_text: "SELECT a.id FROM a CROSS JOIN b", + expected_terminator: "", + expected_ctes: &[], + expected_tables: &[("a", ""), ("b", "")], + expected_joins: &[("CROSS", "b", false, "CROSS JOIN b")], + expected_stars: &[], + expected_functions: &[], + expected_literals: &[], + expected_columns: &[("a", "id")], + }, + StatementFactsTestCase { + description: "join clauses end at the next join or clause, around subqueries and calls", + sql: "SELECT a.id FROM a INNER JOIN (SELECT id FROM b) AS s ON a.id = s.id LEFT JOIN c USING (id) WHERE LEFT(a.name, 1) = 'x'", + statement: 0, + expected_kind: "select", + expected_text: "SELECT a.id FROM a INNER JOIN (SELECT id FROM b) AS s ON a.id = s.id LEFT JOIN c USING (id) WHERE LEFT(a.name, 1) = 'x'", + expected_terminator: "", + expected_ctes: &[], + expected_tables: &[("a", ""), ("b", ""), ("c", "")], + expected_joins: &[ + ( + "INNER", + "", + true, + "INNER JOIN (SELECT id FROM b) AS s ON a.id = s.id", + ), + ("LEFT", "c", true, "LEFT JOIN c USING (id)"), + ], + expected_stars: &[], + expected_functions: &["LEFT"], + expected_literals: &["1", "'x'"], + expected_columns: &[("a", "name"), ("s", "id")], + }, + ]; + for test_case in &test_cases { + let facts: FileFacts = file_facts(test_case.sql, "duckdb")?; + let statement: &StatementFacts = &facts.statements[test_case.statement]; + let located = |start: usize, end: usize| text(test_case.sql, start, end); + assert_eq!( + statement.kind, test_case.expected_kind, + "{}", + test_case.description + ); + assert_eq!( + located(statement.start, statement.end), + test_case.expected_text, + "{}", + test_case.description + ); + assert_eq!( + located(statement.end, statement.terminated_end), + test_case.expected_terminator, + "{}", + test_case.description + ); + let ctes: Vec = statement + .ctes + .iter() + .map(|cte| located(cte.start, cte.end)) + .collect(); + assert_eq!(ctes, test_case.expected_ctes, "{}", test_case.description); + let tables: Vec<(&str, &str)> = statement + .tables + .iter() + .map(|table| { + ( + table.qualified_name.as_str(), + table.alias.as_deref().unwrap_or(""), + ) + }) + .collect(); + assert_eq!( + tables, test_case.expected_tables, + "{}", + test_case.description + ); + let joins: Vec<(&str, &str, bool, String)> = statement + .joins + .iter() + .map(|join| { + ( + join.kind.as_str(), + join.relation.as_deref().unwrap_or(""), + join.has_condition, + located(join.start, join.end), + ) + }) + .collect(); + let expected_joins: Vec<(&str, &str, bool, String)> = test_case + .expected_joins + .iter() + .map(|(kind, relation, condition, text)| { + (*kind, *relation, *condition, (*text).to_owned()) + }) + .collect(); + assert_eq!(joins, expected_joins, "{}", test_case.description); + let stars: Vec = statement + .stars + .iter() + .map(|star| located(star.start, star.end)) + .collect(); + assert_eq!(stars, test_case.expected_stars, "{}", test_case.description); + let functions: Vec = statement + .functions + .iter() + .map(|function| located(function.start, function.end)) + .collect(); + assert_eq!( + functions, test_case.expected_functions, + "{}", + test_case.description + ); + let literals: Vec = statement + .literals + .iter() + .map(|literal| located(literal.start, literal.end)) + .collect(); + assert_eq!( + literals, test_case.expected_literals, + "{}", + test_case.description + ); + let columns: Vec<(&str, &str)> = statement + .columns + .iter() + .map(|column| { + ( + column.qualifier.as_deref().unwrap_or(""), + column.name.as_str(), + ) + }) + .collect(); + assert!( + test_case + .expected_columns + .iter() + .all(|expected| columns.contains(expected)), + "{}: {columns:?}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_sql_text_when_collecting_facts_then_only_unparseable_sql_is_an_error() { + let test_cases = [ + ParseOutcomeTestCase { + description: "a broken statement returns the parse error", + sql: "SELECT FROM WHERE (", + expected_statements: None, + }, + ParseOutcomeTestCase { + description: "two terminated statements both parse", + sql: "SELECT 1; SELECT 2;", + expected_statements: Some(2), + }, + ]; + for test_case in &test_cases { + let statements: Option = file_facts(test_case.sql, "duckdb") + .map(|facts| facts.statements.len()) + .ok(); + assert_eq!( + statements, test_case.expected_statements, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlclean/src/sql_facts/tests/test_types.rs b/crates/sqlclean/src/sql_facts/tests/test_types.rs new file mode 100644 index 0000000..3b3ac78 --- /dev/null +++ b/crates/sqlclean/src/sql_facts/tests/test_types.rs @@ -0,0 +1,21 @@ +pub(crate) struct StatementFactsTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) statement: usize, + pub(crate) expected_kind: &'static str, + pub(crate) expected_text: &'static str, + pub(crate) expected_terminator: &'static str, + pub(crate) expected_ctes: &'static [&'static str], + pub(crate) expected_tables: &'static [(&'static str, &'static str)], + pub(crate) expected_joins: &'static [(&'static str, &'static str, bool, &'static str)], + pub(crate) expected_stars: &'static [&'static str], + pub(crate) expected_functions: &'static [&'static str], + pub(crate) expected_literals: &'static [&'static str], + pub(crate) expected_columns: &'static [(&'static str, &'static str)], +} + +pub(crate) struct ParseOutcomeTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) expected_statements: Option, +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/additional.rs b/crates/sqlclean/src/sql_lint/_helpers/additional.rs new file mode 100644 index 0000000..e562b59 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/additional.rs @@ -0,0 +1,1873 @@ +use std::collections::HashSet; + +use polyglot_sql::tokens::{Span, Token, TokenType}; + +use crate::constants::SQL_MACRO_PARAMETER_SENTINEL_PREFIX; +use crate::constants::{CLOSE_PAREN, OPEN_PAREN, SQL_WILDCARD}; +use crate::constants::{EMPTY_FIXTURE_QUERY_TOKEN_COUNT, ENCLOSING_CTE_TOKEN_COUNT}; +use crate::sql_lint::_helpers::engine::{ + direct_indices, is_comment, is_layout, is_query_from, query_end, significant_after, + significant_before, token_depths, +}; +use crate::sql_lint::constants::RELATION_MODIFIER_KEYWORDS; +use crate::sql_lint::models::{AdditionalFactOptions, AdditionalQueryFacts, SqlMacroOutput}; +use rustc_hash::{FxHashMap, FxHashSet}; + +const COUNT_ONE_LITERAL: &str = "1"; +const QUALIFIED_REFERENCE_LENGTH: usize = 3; +const QUALIFIER_PAIR_LENGTH: usize = 2; +const IMPLICIT_ALIAS_MINIMUM_LENGTH: usize = 2; +const GENERATED_IDENTIFIER_PREFIXES: [&str; 3] = [ + "__sqb_lint_", + "__sqlbuild_audit_parameter_", + "__sqlbuild_context_parameter_", +]; + +struct QuerySlice<'a> { + tokens: &'a [Token], + depths: &'a [usize], + direct: &'a [usize], + start: usize, + end: usize, + depth: usize, +} + +struct ReferenceFactContext<'a> { + query: &'a QuerySlice<'a>, + outer_relations: &'a FxHashSet, + external_identifiers: &'a HashSet, + sql_macro_outputs: &'a [SqlMacroOutput], + query_start: usize, + query_end: usize, +} + +struct ReferenceCandidateContext<'a> { + tokens: &'a [Token], + references: &'a [usize], + external_identifiers: &'a HashSet, + projection_aliases: &'a FxHashMap, +} + +struct SelectFactContext<'a> { + tokens: &'a [Token], + depths: &'a [usize], + external_identifiers: &'a HashSet, + sql_macro_outputs: &'a [SqlMacroOutput], + dependency_identifiers: &'a HashSet, + allows_ceremonial_select: bool, + allows_dynamic_output_star: bool, + allows_empty_fixture_star: bool, +} + +struct ProjectedStarContext<'a> { + tokens: &'a [Token], + depths: &'a [usize], + direct: &'a [usize], + query_end: usize, +} + +pub(super) fn collect_additional_facts( + tokens: &[Token], + options: &AdditionalFactOptions<'_>, +) -> AdditionalQueryFacts { + let mut facts = AdditionalQueryFacts::default(); + let depths = token_depths(tokens); + let significant: Vec = (0..tokens.len()) + .filter(|&index| !is_layout(&tokens[index]) && !is_comment(&tokens[index])) + .collect(); + for (position, &index) in significant.iter().enumerate() { + let token = &tokens[index]; + if token.token_type == TokenType::Union + && significant.get(position + 1).is_none_or(|&next| { + !matches!( + tokens[next].token_type, + TokenType::All | TokenType::Distinct | TokenType::By + ) + }) + { + facts.bare_unions.push(token.span); + } + if token.token_type == TokenType::Else + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].token_type == TokenType::Null) + { + let next = significant[position + 1]; + facts.redundant_else_nulls.push(Span { + start: token.span.start, + end: tokens[next].span.end, + line: token.span.line, + column: token.span.column, + }); + } + if token.token_type == TokenType::Distinct + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].token_type == TokenType::LParen) + && let Some(span) = + parenthesized_distinct_span(tokens, &depths, significant[position + 1]) + { + facts.parenthesized_distincts.push(span); + } + if token.token_type == TokenType::Semicolon + && significant + .get(position.wrapping_sub(1)) + .is_some_and(|&previous| tokens[previous].token_type == TokenType::Semicolon) + { + facts.consecutive_semicolons.push(token.span); + } + if is_identifier(token) + && let Some((&as_index, &alias_index)) = significant + .get(position + 1) + .zip(significant.get(position + 2)) + && tokens[as_index].token_type == TokenType::As + && is_identifier(&tokens[alias_index]) + && token.token_type == tokens[alias_index].token_type + && token.text == tokens[alias_index].text + && is_simple_projection_reference(tokens, &depths, &significant, position) + { + facts.redundant_self_aliases.push(Span { + start: token.span.end, + end: tokens[alias_index].span.end, + line: tokens[as_index].span.line, + column: tokens[as_index].span.column, + }); + } + if token.text.eq_ignore_ascii_case("COUNT") + && let Some(span) = count_one_span(tokens, &significant, position) + { + facts.count_one_literals.push(span); + } + if token.text.eq_ignore_ascii_case("ROW_NUMBER") + && let Some(span) = unstable_row_number_span(tokens, &depths, index) + { + facts.unstable_row_numbers.push(span); + } + if token.token_type == TokenType::Not + && let Some(span) = null_not_in_span(tokens, &depths, &significant, position) + { + facts.null_not_in_predicates.push(span); + } + if token.token_type == TokenType::Case + && let Some(span) = simple_boolean_case_span(tokens, &significant, position) + { + facts.simple_boolean_cases.push(span); + } + if token.token_type == TokenType::Else + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].token_type == TokenType::Case) + { + facts + .nested_else_cases + .push(tokens[significant[position + 1]].span); + } + if position + 2 < significant.len() + && constant_predicate(tokens, &significant[position..position + 3]) + { + facts + .constant_predicates + .push(tokens[significant[position + 1]].span); + } + } + let select_facts = collect_select_additional_facts(&SelectFactContext { + tokens, + depths: &depths, + external_identifiers: options.external_identifiers, + sql_macro_outputs: options.sql_macro_outputs, + dependency_identifiers: options.dependency_identifiers, + allows_ceremonial_select: options.allows_ceremonial_select, + allows_dynamic_output_star: options.allows_dynamic_output_star, + allows_empty_fixture_star: options.allows_empty_fixture_star, + }); + facts.duplicate_output_aliases = select_facts.duplicate_output_aliases; + facts.unaliased_calculations = select_facts.unaliased_calculations; + facts.projected_stars = select_facts.projected_stars; + facts.duplicate_table_aliases = select_facts.duplicate_table_aliases; + facts.unused_table_aliases = select_facts.unused_table_aliases; + facts.null_rejected_left_joins = select_facts.null_rejected_left_joins; + facts.implicit_inner_joins = select_facts.implicit_inner_joins; + facts.unqualified_multi_source_columns = select_facts.unqualified_multi_source_columns; + facts.inconsistent_single_source_qualification = + select_facts.inconsistent_single_source_qualification; + facts.unknown_relation_qualifiers = select_facts.unknown_relation_qualifiers; + facts.unused_joined_relations = select_facts.unused_joined_relations; + facts.mixed_group_order_references = select_facts.mixed_group_order_references; + facts.ambiguous_order_directions = select_facts.ambiguous_order_directions; + facts.set_arity_mismatches = set_arity_mismatch_spans(tokens, &depths); + facts +} + +fn is_simple_projection_reference( + tokens: &[Token], + depths: &[usize], + significant: &[usize], + position: usize, +) -> bool { + let index = significant[position]; + let Some(boundary) = (0..index).rev().find(|&candidate| { + depths[candidate] == depths[index] + && (tokens[candidate].token_type == TokenType::Select + || is_query_from(tokens, candidate) + || tokens[candidate].token_type == TokenType::Join) + }) else { + return false; + }; + if tokens[boundary].token_type != TokenType::Select { + return false; + } + let item_boundary = significant[..position] + .iter() + .rposition(|&candidate| { + candidate > boundary + && depths[candidate] == depths[index] + && tokens[candidate].token_type == TokenType::Comma + }) + .map_or(boundary, |boundary_position| significant[boundary_position]); + let prefix: Vec = significant[..position] + .iter() + .copied() + .filter(|&candidate| candidate > item_boundary && depths[candidate] == depths[index]) + .collect(); + if is_bare_star_prefix(tokens, &prefix) { + return true; + } + let mut qualifier_start = prefix.len(); + while qualifier_start >= QUALIFIER_PAIR_LENGTH + && tokens[prefix[qualifier_start - 1]].token_type == TokenType::Dot + && is_identifier(&tokens[prefix[qualifier_start - 2]]) + { + qualifier_start -= 2; + } + qualifier_start < prefix.len() && is_bare_star_prefix(tokens, &prefix[..qualifier_start]) +} + +fn collect_select_additional_facts(context: &SelectFactContext<'_>) -> AdditionalQueryFacts { + let tokens = context.tokens; + let depths = context.depths; + let mut facts = AdditionalQueryFacts::default(); + for (select_index, token) in tokens.iter().enumerate() { + if token.token_type != TokenType::Select { + continue; + } + let depth = depths[select_index]; + let end = query_end(tokens, depths, select_index, depth); + let direct: Vec = direct_indices(depths, select_index + 1, end, depth) + .into_iter() + .filter(|&index| !is_layout(&tokens[index]) && !is_comment(&tokens[index])) + .collect(); + let from_position = direct + .iter() + .position(|&index| is_query_from(tokens, index)) + .unwrap_or(direct.len()); + facts + .duplicate_output_aliases + .extend(duplicate_alias_spans(tokens, &direct[..from_position])); + if !is_exists_subquery(tokens, select_index) + && !is_set_continuation_select(tokens, select_index) + && !is_scalar_subquery(tokens, depths, select_index) + && !has_enclosing_cte_column_list(tokens, depths, select_index) + && !is_ceremonial_select( + tokens, + &direct[..from_position], + context.allows_ceremonial_select, + ) + { + facts + .unaliased_calculations + .extend(unaliased_calculation_spans( + tokens, + &direct[..from_position], + )); + } + if !is_dependency_import_select(context, select_index, &direct, from_position) + && !is_compiler_proven_dynamic_output_select( + context, + select_index, + &direct, + from_position, + ) + && !is_canonical_empty_input_fixture(context, select_index, end) + && !has_irreducible_projected_star( + &ProjectedStarContext { + tokens, + depths, + direct: &direct, + query_end: end, + }, + from_position, + ) + { + facts + .projected_stars + .extend(projected_star_spans(tokens, &direct[..from_position])); + } + if from_position < direct.len() { + let clause_end = direct[from_position + 1..] + .iter() + .position(|&index| is_after_relation_clause(tokens[index].token_type)) + .map_or(direct.len(), |offset| from_position + 1 + offset); + facts.duplicate_table_aliases.extend(duplicate_alias_spans( + tokens, + &direct[from_position + 1..clause_end], + )); + let query_slice = QuerySlice { + tokens, + depths, + direct: &direct[from_position + 1..clause_end], + start: select_index, + end, + depth, + }; + facts + .unused_table_aliases + .extend(unused_alias_spans(&query_slice)); + facts + .null_rejected_left_joins + .extend(null_rejected_left_join_spans(tokens, &direct, clause_end)); + facts.implicit_inner_joins.extend(implicit_inner_join_spans( + tokens, + &direct[from_position + 1..clause_end], + )); + let reference_slice = QuerySlice { + tokens, + depths, + direct: &direct, + start: from_position, + end: clause_end, + depth, + }; + let outer_relations = outer_relation_names(tokens, depths, select_index, depth); + let reference_context = ReferenceFactContext { + query: &reference_slice, + outer_relations: &outer_relations, + external_identifiers: context.external_identifiers, + sql_macro_outputs: context.sql_macro_outputs, + query_start: select_index, + query_end: end, + }; + let reference_facts = collect_reference_facts(&reference_context); + facts + .unqualified_multi_source_columns + .extend(reference_facts.unqualified_multi_source_columns); + facts + .inconsistent_single_source_qualification + .extend(reference_facts.inconsistent_single_source_qualification); + facts + .unknown_relation_qualifiers + .extend(reference_facts.unknown_relation_qualifiers); + facts + .unused_joined_relations + .extend(reference_facts.unused_joined_relations); + } + facts + .mixed_group_order_references + .extend(mixed_reference_clause_spans(tokens, &direct)); + facts + .ambiguous_order_directions + .extend(ambiguous_order_direction_spans(tokens, &direct)); + } + facts +} + +fn is_canonical_empty_input_fixture( + context: &SelectFactContext<'_>, + select_index: usize, + query_end: usize, +) -> bool { + if !context.allows_empty_fixture_star { + return false; + } + let significant_query: Vec<&str> = context.tokens[select_index..query_end] + .iter() + .filter(|token| !is_layout(token) && !is_comment(token)) + .map(|token| token.text.as_str()) + .collect(); + if significant_query.len() != EMPTY_FIXTURE_QUERY_TOKEN_COUNT + || !significant_query[0].eq_ignore_ascii_case("select") + || significant_query[1] != SQL_WILDCARD + || !significant_query[2].eq_ignore_ascii_case("from") + || !significant_query[3].eq_ignore_ascii_case("__empty_fixture") + || significant_query[4] != OPEN_PAREN + || significant_query[5] != CLOSE_PAREN + { + return false; + } + let previous: Vec<&Token> = context.tokens[..select_index] + .iter() + .filter(|token| !is_layout(token) && !is_comment(token)) + .rev() + .take(ENCLOSING_CTE_TOKEN_COUNT) + .collect(); + if previous.len() != ENCLOSING_CTE_TOKEN_COUNT + || previous[0].token_type != TokenType::LParen + || !previous[1].text.eq_ignore_ascii_case("as") + { + return false; + } + let cte_name = previous[2].text.trim_matches(['"', '`']); + ["__ref__", "__source__", "__seed__"] + .iter() + .any(|prefix| cte_name.starts_with(prefix)) +} + +fn is_compiler_proven_dynamic_output_select( + context: &SelectFactContext<'_>, + select_index: usize, + direct: &[usize], + from_position: usize, +) -> bool { + if !context.allows_dynamic_output_star + || context.depths[select_index] != 0 + || direct[..from_position].len() != 1 + || context.tokens[direct[0]].token_type != TokenType::Star + { + return false; + } + !context.tokens[select_index + 1..] + .iter() + .zip(&context.depths[select_index + 1..]) + .any(|(token, depth)| token.token_type == TokenType::Select && *depth == 0) +} + +fn has_irreducible_projected_star( + context: &ProjectedStarContext<'_>, + from_position: usize, +) -> bool { + let ProjectedStarContext { + tokens, + depths, + direct, + query_end, + } = context; + let projection = &direct[..from_position]; + if projection.len() != 1 || tokens[projection[0]].token_type != TokenType::Star { + return false; + } + + let Some(relation_start) = from_position + .checked_add(1) + .filter(|&start| start < direct.len()) + else { + return false; + }; + let relation_end = direct[relation_start..] + .iter() + .position(|&index| is_after_relation_clause(tokens[index].token_type)) + .map_or(direct.len(), |offset| relation_start + offset); + let relation = &direct[relation_start..relation_end]; + if relation + .iter() + .any(|&index| matches!(tokens[index].token_type, TokenType::Join | TokenType::Comma)) + { + return false; + } + if is_lone_audit_parameter_relation(tokens, relation) { + return true; + } + + let mut pivots = relation + .iter() + .copied() + .filter(|&index| tokens[index].text.eq_ignore_ascii_case("PIVOT")); + let Some(pivot_index) = pivots.next() else { + return false; + }; + if pivots.next().is_some() { + return false; + } + dynamic_pivot_uses_any(tokens, depths, pivot_index, *query_end) +} + +fn is_lone_audit_parameter_relation(tokens: &[Token], relation: &[usize]) -> bool { + let Some(&relation_index) = relation.first() else { + return false; + }; + let normalized = tokens[relation_index].text.to_ascii_lowercase(); + let Some(index) = normalized + .strip_prefix("__sqlbuild_audit_parameter_") + .and_then(|value| value.strip_suffix("__")) + else { + return false; + }; + if index.is_empty() || !index.chars().all(|character| character.is_ascii_digit()) { + return false; + } + match relation.get(1..) { + Some([]) => true, + Some([alias]) => is_identifier(&tokens[*alias]), + Some([as_index, alias]) => { + tokens[*as_index].token_type == TokenType::As && is_identifier(&tokens[*alias]) + } + _ => false, + } +} + +fn dynamic_pivot_uses_any( + tokens: &[Token], + depths: &[usize], + pivot_index: usize, + query_end: usize, +) -> bool { + let Some(pivot_open) = significant_after(tokens, pivot_index) else { + return false; + }; + if tokens[pivot_open].token_type != TokenType::LParen { + return false; + } + let pivot_depth = depths[pivot_open]; + let Some(pivot_close) = (pivot_open + 1..query_end).find(|&index| { + depths[index] == pivot_depth + 1 && tokens[index].token_type == TokenType::RParen + }) else { + return false; + }; + (pivot_open + 1..pivot_close).any(|in_index| { + depths[in_index] == pivot_depth + 1 + && tokens[in_index].token_type == TokenType::In + && significant_after(tokens, in_index).is_some_and(|any_open| { + any_open < pivot_close + && tokens[any_open].token_type == TokenType::LParen + && significant_after(tokens, any_open).is_some_and(|any_index| { + any_index < pivot_close && tokens[any_index].token_type == TokenType::Any + }) + }) + }) +} + +fn has_enclosing_cte_column_list(tokens: &[Token], depths: &[usize], select_index: usize) -> bool { + let select_depth = depths[select_index]; + let Some(body_open) = (0..select_index).rev().find(|&index| { + tokens[index].token_type == TokenType::LParen + && depths[index].saturating_add(1) == select_depth + }) else { + return false; + }; + let Some(as_index) = significant_before(tokens, body_open) else { + return false; + }; + if tokens[as_index].token_type != TokenType::As { + return false; + } + significant_before(tokens, as_index) + .is_some_and(|index| tokens[index].token_type == TokenType::RParen) +} + +fn is_dependency_import_select( + context: &SelectFactContext<'_>, + select_index: usize, + direct: &[usize], + from_position: usize, +) -> bool { + let tokens = context.tokens; + let depths = context.depths; + if from_position != 1 + || tokens[direct[0]].token_type != TokenType::Star + || !is_top_level_cte_body(tokens, depths, select_index) + { + return false; + } + if direct.len() <= from_position + 1 || has_cte_body_set_operation(tokens, depths, select_index) + { + return false; + } + let relation = &direct[from_position + 1..]; + let Some(&relation_index) = relation.first() else { + return false; + }; + if !context + .dependency_identifiers + .contains(&tokens[relation_index].text.to_ascii_lowercase()) + { + return false; + } + match relation { + [_] => true, + [_, alias] => is_identifier(&tokens[*alias]), + [_, explicit_as, alias] => { + tokens[*explicit_as].token_type == TokenType::As && is_identifier(&tokens[*alias]) + } + _ => false, + } +} + +fn has_cte_body_set_operation(tokens: &[Token], depths: &[usize], select_index: usize) -> bool { + let select_depth = depths[select_index]; + let body_end = (select_index + 1..tokens.len()) + .find(|&index| { + depths[index] == select_depth && tokens[index].token_type == TokenType::RParen + }) + .unwrap_or(tokens.len()); + (select_index + 1..body_end).any(|index| { + depths[index] == select_depth + && matches!( + tokens[index].token_type, + TokenType::Union | TokenType::Intersect | TokenType::Except + ) + }) +} + +fn is_top_level_cte_body(tokens: &[Token], depths: &[usize], select_index: usize) -> bool { + let Some(open_index) = significant_before(tokens, select_index) else { + return false; + }; + if tokens[open_index].token_type != TokenType::LParen || depths[open_index] != 0 { + return false; + } + significant_before(tokens, open_index) + .is_some_and(|index| tokens[index].token_type == TokenType::As) +} + +fn collect_reference_facts(context: &ReferenceFactContext<'_>) -> AdditionalQueryFacts { + let query = context.query; + let QuerySlice { + tokens, + direct, + start: from_position, + end: relation_end, + .. + } = query; + let from_position = *from_position; + let relation_end = *relation_end; + let mut facts = AdditionalQueryFacts::default(); + let relations = &direct[from_position + 1..relation_end]; + let relation_count = if is_values_relation(tokens, relations) { + 1 + } else { + 1 + relations + .iter() + .filter(|&&index| { + matches!(tokens[index].token_type, TokenType::Join | TokenType::Comma) + }) + .count() + }; + let mut known = relation_names(tokens, relations); + known.extend(context.outer_relations.iter().cloned()); + let references: Vec = direct[..from_position] + .iter() + .chain(direct[relation_end..].iter()) + .copied() + .collect(); + let projection_aliases = projection_alias_positions(tokens, &direct[..from_position]); + let candidate_context = ReferenceCandidateContext { + tokens, + references: &references, + external_identifiers: context.external_identifiers, + projection_aliases: &projection_aliases, + }; + let qualified: Vec = references + .iter() + .enumerate() + .filter_map(|(position, &index)| simple_qualifier(tokens, &references, position, index)) + .collect(); + let unqualified: Vec = references + .iter() + .enumerate() + .filter_map(|(position, &index)| unqualified_reference(&candidate_context, position, index)) + .collect(); + if relation_count > 1 { + facts + .unqualified_multi_source_columns + .extend(unqualified.iter().map(|&index| tokens[index].span)); + } else { + facts + .inconsistent_single_source_qualification + .extend(inconsistent_qualification( + tokens, + (&qualified, &unqualified), + context.sql_macro_outputs, + )); + } + facts.unknown_relation_qualifiers.extend( + qualified + .iter() + .filter(|&&index| !known.contains(&tokens[index].text.to_ascii_lowercase())) + .map(|&index| tokens[index].span), + ); + let projects_bare_star = has_bare_projected_star(tokens, &direct[..from_position]); + if !projects_bare_star { + facts.unused_joined_relations.extend(unused_join_spans( + query, + context.query_start, + context.query_end, + )); + } + facts +} + +/// The unqualified references of a single-relation scope that also qualifies some, each judged as it was written. In a macro file a plain slot read such as `@orders.amount` lints as a parameter sentinel pair and counts as unqualified. In a call's output, a read qualified by a relation the call passes takes neither style: the slot decides how it expands, not the caller or the macro author. A scope whose every reference a macro call generated is judged once, in the macro file. +fn inconsistent_qualification( + tokens: &[Token], + (qualified, unqualified): (&[usize], &[usize]), + outputs: &[SqlMacroOutput], +) -> Vec { + let output_of = |index: usize| -> Option { + let start: usize = tokens[index].span.start; + outputs + .iter() + .position(|output| output.start <= start && start < output.end) + }; + let mut references = qualified.iter().chain(unqualified).copied(); + if let Some(first) = references.next() + && let Some(output) = output_of(first) + && references.all(|index| output_of(index) == Some(output)) + { + return Vec::new(); + } + let passed_relation_read = |index: &usize| { + output_of(*index).is_some_and(|output| { + let qualifier: String = unquoted_name(&tokens[*index].text); + outputs[output] + .relations + .iter() + .any(|relation| relation.eq_ignore_ascii_case(&qualifier)) + }) + }; + let (plain_reads, qualified): (Vec, Vec) = qualified + .iter() + .copied() + .filter(|index| !passed_relation_read(index)) + .partition(|&index| is_plain_slot_read(tokens, index)); + if qualified.is_empty() || (unqualified.is_empty() && plain_reads.is_empty()) { + return Vec::new(); + } + let mut unqualified: Vec = unqualified.to_vec(); + unqualified.extend(plain_reads); + unqualified.sort_unstable(); + unqualified + .iter() + .map(|&index| tokens[index].span) + .collect() +} + +/// An identifier's name without its quotes. +fn unquoted_name(text: &str) -> String { + text.trim_matches(|character| matches!(character, '"' | '`' | '[' | ']')) + .to_owned() +} + +/// Whether the qualified reference at `index` is a plain SQL macro slot read, both its relation and its slot a macro parameter. +fn is_plain_slot_read(tokens: &[Token], index: usize) -> bool { + let is_parameter = |token: &Token| { + token + .text + .to_ascii_lowercase() + .starts_with(SQL_MACRO_PARAMETER_SENTINEL_PREFIX) + }; + is_parameter(&tokens[index]) && tokens.get(index + 2).is_some_and(is_parameter) +} + +fn simple_qualifier( + tokens: &[Token], + references: &[usize], + position: usize, + index: usize, +) -> Option { + let previous = position.checked_sub(1).map(|value| references[value]); + let dot = references.get(position + 1).copied(); + let column = references.get(position + 2).copied(); + let following = references.get(position + 3).copied(); + (is_identifier(&tokens[index]) + && !is_generated_identifier(&tokens[index]) + && previous.is_none_or(|candidate| { + !matches!( + tokens[candidate].token_type, + TokenType::Dot | TokenType::Colon | TokenType::DColon | TokenType::DotColon + ) + }) + && dot.is_some_and(|candidate| tokens[candidate].token_type == TokenType::Dot) + && column.is_some_and(|candidate| is_identifier(&tokens[candidate])) + && following.is_none_or(|candidate| tokens[candidate].token_type != TokenType::Dot)) + .then_some(index) +} + +fn unqualified_reference( + context: &ReferenceCandidateContext<'_>, + position: usize, + index: usize, +) -> Option { + let ReferenceCandidateContext { + tokens, + references, + external_identifiers, + projection_aliases, + } = context; + let previous = position.checked_sub(1).map(|value| references[value]); + let next = references.get(position + 1).copied(); + (is_identifier(&tokens[index]) + && !is_generated_identifier(&tokens[index]) + && !is_context_value(&tokens[index]) + && !external_identifiers.contains(&tokens[index].text.to_ascii_lowercase()) + && !projection_aliases + .get(&tokens[index].text.to_ascii_lowercase()) + .is_some_and(|alias_index| *alias_index < index) + && previous.is_none_or(|candidate| { + !matches!( + tokens[candidate].token_type, + TokenType::Dot + | TokenType::As + | TokenType::Colon + | TokenType::DColon + | TokenType::DotColon + ) + }) + && next.is_none_or(|candidate| { + !matches!( + tokens[candidate].token_type, + TokenType::Dot + | TokenType::LParen + | TokenType::Colon + | TokenType::DColon + | TokenType::DotColon + ) + })) + .then_some(index) +} + +fn relation_names(tokens: &[Token], relation_clause: &[usize]) -> FxHashSet { + let mut names: FxHashSet = FxHashSet::default(); + for (position, &index) in relation_clause.iter().enumerate() { + let starts_relation = position == 0 + || matches!( + tokens[relation_clause[position - 1]].token_type, + TokenType::Join | TokenType::Comma + ); + if starts_relation && is_identifier(&tokens[index]) { + names.insert(tokens[index].text.to_ascii_lowercase()); + } + if tokens[index].token_type == TokenType::As + && let Some(&alias) = relation_clause.get(position + 1) + && is_identifier(&tokens[alias]) + { + names.insert(tokens[alias].text.to_ascii_lowercase()); + } + } + let mut segment_start = 0_usize; + for segment_end in relation_clause + .iter() + .enumerate() + .filter_map(|(position, &index)| { + matches!(tokens[index].token_type, TokenType::Join | TokenType::Comma) + .then_some(position) + }) + .chain(std::iter::once(relation_clause.len())) + { + let segment = &relation_clause[segment_start..segment_end]; + segment_start = segment_end + 1; + let relation_end = segment + .iter() + .position(|&index| is_relation_condition_start(&tokens[index])) + .unwrap_or(segment.len()); + if let Some(&implicit_name) = segment[..relation_end] + .iter() + .rev() + .find(|&&index| is_identifier(&tokens[index])) + { + names.insert(tokens[implicit_name].text.to_ascii_lowercase()); + } + } + names +} + +fn outer_relation_names( + tokens: &[Token], + depths: &[usize], + select_index: usize, + depth: usize, +) -> FxHashSet { + let mut names: FxHashSet = FxHashSet::default(); + for outer_select in (0..select_index).filter(|&index| { + tokens[index].token_type == TokenType::Select + && depths[index] < depth + && query_end(tokens, depths, index, depths[index]) > select_index + }) { + let outer_depth = depths[outer_select]; + let outer_end = query_end(tokens, depths, outer_select, outer_depth); + let direct: Vec = direct_indices(depths, outer_select + 1, outer_end, outer_depth) + .into_iter() + .filter(|&index| !is_layout(&tokens[index]) && !is_comment(&tokens[index])) + .collect(); + let Some(from_position) = direct + .iter() + .position(|&index| is_query_from(tokens, index)) + else { + continue; + }; + let clause_end = direct[from_position + 1..] + .iter() + .position(|&index| is_after_relation_clause(tokens[index].token_type)) + .map_or(direct.len(), |offset| from_position + 1 + offset); + names.extend(relation_names( + tokens, + &direct[from_position + 1..clause_end], + )); + } + names +} + +fn projected_star_spans(tokens: &[Token], projection: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (position, &index) in projection.iter().enumerate() { + if tokens[index].token_type != TokenType::Star { + continue; + } + let previous = position.checked_sub(1).map(|value| projection[value]); + let qualified = + previous.is_some_and(|candidate| tokens[candidate].token_type == TokenType::Dot); + let mut item_start = 0_usize; + for prior_position in (0..position).rev() { + if tokens[projection[prior_position]].token_type == TokenType::Comma { + item_start = prior_position + 1; + break; + } + } + let bare = is_bare_star_prefix(tokens, &projection[item_start..position]); + if qualified || bare { + spans.push(tokens[index].span); + } + } + spans +} + +fn has_bare_projected_star(tokens: &[Token], projection: &[usize]) -> bool { + projection.iter().enumerate().any(|(position, &index)| { + if tokens[index].token_type != TokenType::Star { + return false; + } + let mut item_start = 0_usize; + for prior_position in (0..position).rev() { + if tokens[projection[prior_position]].token_type == TokenType::Comma { + item_start = prior_position + 1; + break; + } + } + is_bare_star_prefix(tokens, &projection[item_start..position]) + }) +} + +fn is_exists_subquery(tokens: &[Token], select_index: usize) -> bool { + let Some(open_index) = significant_before(tokens, select_index) else { + return false; + }; + tokens[open_index].token_type == TokenType::LParen + && significant_before(tokens, open_index) + .is_some_and(|index| tokens[index].token_type == TokenType::Exists) +} + +fn is_set_continuation_select(tokens: &[Token], select_index: usize) -> bool { + let mut previous = significant_before(tokens, select_index); + if previous.is_some_and(|index| { + matches!( + tokens[index].token_type, + TokenType::All | TokenType::Distinct + ) + }) { + previous = previous.and_then(|index| significant_before(tokens, index)); + } + previous.is_some_and(|index| { + matches!( + tokens[index].token_type, + TokenType::Union | TokenType::Intersect | TokenType::Except + ) + }) +} + +fn is_bare_star_prefix(tokens: &[Token], prefix: &[usize]) -> bool { + let mut position = 0_usize; + while prefix.get(position).is_some_and(|&index| { + matches!( + tokens[index].token_type, + TokenType::Distinct | TokenType::All + ) + }) { + position += 1; + } + if position == prefix.len() { + return true; + } + if tokens[prefix[position]].token_type != TokenType::Top { + return false; + } + position += 1; + if !prefix + .get(position) + .is_some_and(|&index| tokens[index].token_type == TokenType::Number) + { + return false; + } + position += 1; + if prefix + .get(position) + .is_some_and(|&index| tokens[index].token_type == TokenType::Percent) + { + position += 1; + } + if prefix + .get(position) + .is_some_and(|&index| tokens[index].token_type == TokenType::With) + && prefix + .get(position + 1) + .is_some_and(|&index| tokens[index].token_type == TokenType::Ties) + { + position += 2; + } + position == prefix.len() +} + +fn is_scalar_subquery(tokens: &[Token], depths: &[usize], select_index: usize) -> bool { + let Some(open_index) = significant_before(tokens, select_index) else { + return false; + }; + if tokens[open_index].token_type != TokenType::LParen { + return false; + } + let Some(previous) = significant_before(tokens, open_index) else { + return false; + }; + if matches!( + tokens[previous].token_type, + TokenType::From | TokenType::Join | TokenType::As | TokenType::Exists | TokenType::Lateral + ) { + return false; + } + if tokens[previous].token_type != TokenType::Comma { + return true; + } + let parent_depth = depths[previous]; + let query_start = (0..previous) + .rev() + .find(|&index| { + tokens[index].token_type == TokenType::Select && depths[index] < parent_depth + }) + .map_or(0, |index| index + 1); + let relation_boundary = (query_start..previous).rev().find(|&index| { + depths[index] == parent_depth + && (tokens[index].token_type == TokenType::Select + || is_query_from(tokens, index) + || matches!( + tokens[index].token_type, + TokenType::Join | TokenType::Semicolon + )) + }); + !relation_boundary.is_some_and(|index| { + is_query_from(tokens, index) || tokens[index].token_type == TokenType::Join + }) +} + +fn is_ceremonial_select(tokens: &[Token], projection: &[usize], allowed: bool) -> bool { + allowed + && projection.len() == 1 + && tokens[projection[0]].token_type == TokenType::Number + && tokens[projection[0]].text == COUNT_ONE_LITERAL +} + +fn is_values_relation(tokens: &[Token], relation_clause: &[usize]) -> bool { + relation_clause + .first() + .is_some_and(|&index| tokens[index].token_type == TokenType::Values) +} + +fn is_generated_identifier(token: &Token) -> bool { + let normalized = token.text.to_ascii_lowercase(); + GENERATED_IDENTIFIER_PREFIXES + .iter() + .any(|prefix| normalized.starts_with(prefix)) +} + +fn is_relation_condition_start(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::On | TokenType::Using | TokenType::MatchCondition + ) || token.text.eq_ignore_ascii_case("MATCH_CONDITION") +} + +fn is_context_value(token: &Token) -> bool { + matches!( + token.text.to_ascii_uppercase().as_str(), + "CURRENT_CATALOG" + | "CURRENT_DATE" + | "CURRENT_DATETIME" + | "CURRENT_ROLE" + | "CURRENT_SCHEMA" + | "CURRENT_TIME" + | "CURRENT_TIMESTAMP" + | "CURRENT_USER" + ) +} + +fn projection_alias_positions(tokens: &[Token], projection: &[usize]) -> FxHashMap { + let mut aliases: FxHashMap = FxHashMap::default(); + for window in projection.windows(2) { + if tokens[window[0]].token_type == TokenType::As && is_identifier(&tokens[window[1]]) { + aliases.insert(tokens[window[1]].text.to_ascii_lowercase(), window[1]); + } + } + aliases +} + +fn unused_join_spans(query: &QuerySlice<'_>, query_start: usize, query_end: usize) -> Vec { + let QuerySlice { + tokens, + direct, + start: from_position, + end: relation_end, + .. + } = query; + let from_position = *from_position; + let relation_end = *relation_end; + let relation_start_index = direct.get(from_position + 1).copied().unwrap_or(query_end); + let relation_end_index = direct.get(relation_end).copied().unwrap_or(query_end); + let references: Vec = (query_start..relation_start_index) + .chain(relation_end_index..query_end) + .filter(|&index| !is_layout(&tokens[index]) && !is_comment(&tokens[index])) + .collect(); + if contains_possible_unqualified_reference(tokens, &references) { + return Vec::new(); + } + let mut spans: Vec = Vec::new(); + for position in from_position + 1..relation_end { + let index = direct[position]; + if tokens[index].token_type != TokenType::Join { + continue; + } + let Some(alias) = relation_alias_after_join(tokens, direct, position) else { + continue; + }; + let used_outside_relation_clause = + contains_qualified_reference(tokens, &references, &alias); + let downstream_relation_start = direct[position + 1..relation_end] + .iter() + .position(|&candidate| { + matches!( + tokens[candidate].token_type, + TokenType::Join | TokenType::Comma + ) + }) + .map(|offset| position + 1 + offset); + let used_by_downstream_relation = downstream_relation_start.is_some_and(|start| { + let downstream_start_index = direct[start]; + let downstream_end_index = direct.get(relation_end).copied().unwrap_or(query_end); + let downstream_references: Vec = (downstream_start_index..downstream_end_index) + .filter(|&candidate| { + !is_layout(&tokens[candidate]) && !is_comment(&tokens[candidate]) + }) + .collect(); + contains_qualified_reference(tokens, &downstream_references, &alias) + }); + let used = used_outside_relation_clause || used_by_downstream_relation; + if !used { + spans.push(tokens[index].span); + } + } + spans +} + +fn contains_possible_unqualified_reference(tokens: &[Token], references: &[usize]) -> bool { + references.iter().enumerate().any(|(position, &index)| { + if !is_identifier(&tokens[index]) + || is_generated_identifier(&tokens[index]) + || is_context_value(&tokens[index]) + { + return false; + } + let previous = if position == 0 { + None + } else { + Some(references[position - 1]) + }; + let next = references.get(position + 1).copied(); + previous.is_none_or(|candidate| { + !matches!(tokens[candidate].token_type, TokenType::Dot | TokenType::As) + }) && next.is_none_or(|candidate| { + !matches!( + tokens[candidate].token_type, + TokenType::Dot | TokenType::LParen + ) + }) + }) +} + +fn contains_qualified_reference(tokens: &[Token], references: &[usize], alias: &str) -> bool { + references.windows(2).any(|window| { + tokens[window[0]].text.eq_ignore_ascii_case(alias) + && tokens[window[1]].token_type == TokenType::Dot + }) +} + +fn is_after_relation_clause(token_type: TokenType) -> bool { + matches!( + token_type, + TokenType::Where + | TokenType::Group + | TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + | TokenType::Fetch + | TokenType::Union + | TokenType::Intersect + | TokenType::Except + | TokenType::Semicolon + ) +} + +fn unaliased_calculation_spans(tokens: &[Token], projection: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + let mut start = 0_usize; + let ends: Vec = projection + .iter() + .enumerate() + .filter_map(|(position, &index)| { + (tokens[index].token_type == TokenType::Comma).then_some(position) + }) + .chain(std::iter::once(projection.len())) + .collect(); + for end in ends { + let item = &projection[start..end]; + start = end + 1; + if item.is_empty() + || item + .iter() + .any(|&index| tokens[index].token_type == TokenType::As) + || item + .iter() + .any(|&index| is_generated_identifier(&tokens[index])) + || simple_projection(tokens, item) + || implicit_alias_projection(tokens, item) + { + continue; + } + spans.push(tokens[item[0]].span); + } + spans +} + +fn simple_projection(tokens: &[Token], item: &[usize]) -> bool { + let trimmed: Vec = item + .iter() + .copied() + .skip_while(|&index| { + matches!( + tokens[index].token_type, + TokenType::Distinct | TokenType::All + ) + }) + .collect(); + let wildcard = trimmed.first().is_some_and(|&index| { + tokens[index].token_type == TokenType::Star + || (trimmed.len() >= QUALIFIED_REFERENCE_LENGTH + && is_identifier(&tokens[index]) + && tokens[trimmed[1]].token_type == TokenType::Dot + && tokens[trimmed[2]].token_type == TokenType::Star) + }); + wildcard + || trimmed.len() == 1 + && (is_bare_column_token(&tokens[trimmed[0]]) + || tokens[trimmed[0]].token_type == TokenType::Star) + || trimmed.len() == QUALIFIED_REFERENCE_LENGTH + && is_identifier(&tokens[trimmed[0]]) + && tokens[trimmed[1]].token_type == TokenType::Dot + && (is_bare_column_token(&tokens[trimmed[2]]) + || tokens[trimmed[2]].token_type == TokenType::Star) +} + +fn is_bare_column_token(token: &Token) -> bool { + is_identifier(token) + || (!matches!( + token.token_type, + TokenType::Null + | TokenType::True + | TokenType::False + | TokenType::Number + | TokenType::String + ) && token + .text + .chars() + .next() + .is_some_and(|character| character.is_alphabetic() || character == '_') + && token + .text + .chars() + .all(|character| character.is_alphanumeric() || character == '_')) +} + +fn implicit_alias_projection(tokens: &[Token], item: &[usize]) -> bool { + item.len() >= IMPLICIT_ALIAS_MINIMUM_LENGTH + && is_identifier(&tokens[*item.last().unwrap_or(&item[0])]) + && !matches!( + tokens[item[item.len() - 2]].token_type, + TokenType::Dot | TokenType::Plus | TokenType::Dash | TokenType::Star | TokenType::Slash + ) +} + +fn unused_alias_spans(query: &QuerySlice<'_>) -> Vec { + let QuerySlice { + tokens, + depths, + direct: relation_clause, + start: query_start, + end: query_end, + depth, + } = query; + let query_start = *query_start; + let query_end = *query_end; + let depth = *depth; + let mut spans: Vec = Vec::new(); + for (span_start, alias_index) in relation_alias_positions(tokens, relation_clause) { + if significant_after(tokens, alias_index) + .is_some_and(|index| tokens[index].token_type == TokenType::LParen) + { + continue; + } + let alias = &tokens[alias_index].text; + let relation_name = significant_before(tokens, span_start); + let used = (query_start..query_end).any(|index| { + index != alias_index + && Some(index) != relation_name + && depths[index] >= depth + && is_identifier(&tokens[index]) + && tokens[index].text.eq_ignore_ascii_case(alias) + && significant_before(tokens, index).is_none_or(|previous| { + !matches!(tokens[previous].token_type, TokenType::Dot | TokenType::As) + }) + && significant_after(tokens, index) + .is_none_or(|next| tokens[next].token_type != TokenType::LParen) + }); + if !used { + spans.push(Span { + start: tokens[span_start].span.start, + end: tokens[alias_index].span.end, + line: tokens[span_start].span.line, + column: tokens[span_start].span.column, + }); + } + } + spans +} + +fn relation_alias_positions(tokens: &[Token], relation_clause: &[usize]) -> Vec<(usize, usize)> { + let mut aliases: Vec<(usize, usize)> = Vec::new(); + let mut segment_start = 0_usize; + let mut in_condition = false; + for (position, &index) in relation_clause.iter().enumerate() { + if matches!(tokens[index].token_type, TokenType::Join | TokenType::Comma) { + segment_start = position + 1; + in_condition = false; + continue; + } + if in_condition || is_relation_condition_start(&tokens[index]) { + in_condition = true; + continue; + } + if tokens[index].token_type == TokenType::As { + if let Some(&alias) = relation_clause.get(position + 1) + && is_identifier(&tokens[alias]) + { + aliases.push((index, alias)); + } + continue; + } + if is_implicit_relation_alias( + tokens, + &relation_clause[segment_start..], + position - segment_start, + ) { + aliases.push((index, index)); + } + } + aliases +} + +fn is_implicit_relation_alias(tokens: &[Token], segment: &[usize], position: usize) -> bool { + let candidate = &tokens[segment[position]]; + if position == 0 + || !is_identifier(candidate) + || candidate.token_type != TokenType::QuotedIdentifier + && RELATION_MODIFIER_KEYWORDS + .iter() + .any(|keyword| candidate.text.eq_ignore_ascii_case(keyword)) + { + return false; + } + let previous = &tokens[segment[position - 1]]; + if previous.token_type == TokenType::LParen { + return true; + } + is_identifier(previous) + && position + .checked_sub(QUALIFIER_PAIR_LENGTH) + .is_none_or(|before| tokens[segment[before]].token_type == TokenType::Dot) +} + +fn null_rejected_left_join_spans( + tokens: &[Token], + direct: &[usize], + relation_end: usize, +) -> Vec { + let Some(where_position) = direct + .iter() + .position(|&index| tokens[index].token_type == TokenType::Where) + else { + return Vec::new(); + }; + if direct[where_position + 1..] + .iter() + .any(|&index| tokens[index].token_type == TokenType::Or) + { + return Vec::new(); + } + let mut spans: Vec = Vec::new(); + for position in 0..relation_end { + let index = direct[position]; + if tokens[index].token_type != TokenType::Left { + continue; + } + let Some(join_position) = direct[position + 1..relation_end] + .iter() + .position(|&candidate| tokens[candidate].token_type == TokenType::Join) + .map(|offset| position + 1 + offset) + else { + continue; + }; + let Some(alias) = relation_alias_after_join(tokens, direct, join_position) else { + continue; + }; + if right_side_null_rejected(tokens, &direct[where_position + 1..], &alias) { + spans.push(tokens[index].span); + } + } + spans +} + +fn right_side_null_rejected(tokens: &[Token], predicate: &[usize], alias: &str) -> bool { + for window in predicate.windows(4) { + if !tokens[window[0]].text.eq_ignore_ascii_case(alias) + || tokens[window[1]].token_type != TokenType::Dot + || !is_identifier(&tokens[window[2]]) + { + continue; + } + if matches!( + tokens[window[3]].token_type, + TokenType::Eq + | TokenType::Neq + | TokenType::Lt + | TokenType::Lte + | TokenType::Gt + | TokenType::Gte + | TokenType::Like + | TokenType::In + | TokenType::Between + ) { + return true; + } + } + for window in predicate.windows(6) { + if tokens[window[0]].text.eq_ignore_ascii_case(alias) + && tokens[window[1]].token_type == TokenType::Dot + && is_identifier(&tokens[window[2]]) + && tokens[window[3]].token_type == TokenType::Is + && tokens[window[4]].token_type == TokenType::Not + && tokens[window[5]].token_type == TokenType::Null + { + return true; + } + } + false +} + +fn relation_alias_after_join( + tokens: &[Token], + direct: &[usize], + join_position: usize, +) -> Option { + let relation = &direct[join_position + 1..]; + let relation_end = relation + .iter() + .position(|&index| { + is_relation_condition_start(&tokens[index]) + || is_after_relation_clause(tokens[index].token_type) + || matches!(tokens[index].token_type, TokenType::Join | TokenType::Comma) + }) + .unwrap_or(relation.len()); + relation[..relation_end] + .iter() + .rev() + .find(|&&index| is_identifier(&tokens[index])) + .map(|&index| tokens[index].text.to_ascii_lowercase()) +} + +fn implicit_inner_join_spans(tokens: &[Token], relation_clause: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (position, &index) in relation_clause.iter().enumerate() { + if is_plain_conditioned_join(tokens, relation_clause, position, index) { + spans.push(tokens[index].span); + } + } + spans +} + +fn is_plain_conditioned_join( + tokens: &[Token], + relation_clause: &[usize], + position: usize, + index: usize, +) -> bool { + if tokens[index].token_type != TokenType::Join + || position > 0 + && matches!( + tokens[relation_clause[position - 1]].token_type, + TokenType::Inner + | TokenType::Left + | TokenType::Right + | TokenType::Full + | TokenType::Outer + | TokenType::Cross + | TokenType::Natural + | TokenType::AsOf + | TokenType::Semi + | TokenType::Anti + ) + { + return false; + } + for &candidate in &relation_clause[position + 1..] { + if tokens[candidate].token_type == TokenType::Join { + break; + } + if matches!( + tokens[candidate].token_type, + TokenType::On | TokenType::Using + ) { + return true; + } + } + false +} + +fn ambiguous_order_direction_spans(tokens: &[Token], direct: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (position, &index) in direct.iter().enumerate() { + if tokens[index].token_type != TokenType::Order + || direct + .get(position + 1) + .is_none_or(|&by| tokens[by].token_type != TokenType::By) + { + continue; + } + let end = direct[position + 2..] + .iter() + .position(|&candidate| is_clause_boundary(tokens[candidate].token_type)) + .map_or(direct.len(), |offset| position + 2 + offset); + let clause = &direct[position + 2..end]; + let item_ends: Vec = clause + .iter() + .enumerate() + .filter_map(|(item_position, &candidate)| { + (tokens[candidate].token_type == TokenType::Comma).then_some(item_position) + }) + .chain(std::iter::once(clause.len())) + .collect(); + let mut item_start = 0_usize; + let mut explicit = false; + let mut implicit = false; + for item_end in item_ends { + let item = &clause[item_start..item_end]; + item_start = item_end + 1; + let has_direction = item.last().is_some_and(|&candidate| { + matches!( + tokens[candidate].token_type, + TokenType::Asc | TokenType::Desc + ) + }); + explicit |= has_direction; + implicit |= !has_direction && !item.is_empty(); + } + if explicit && implicit { + spans.push(tokens[index].span); + } + } + spans +} + +fn duplicate_alias_spans(tokens: &[Token], indices: &[usize]) -> Vec { + let mut seen: FxHashSet = FxHashSet::default(); + let mut duplicates: Vec = Vec::new(); + for window in indices.windows(2) { + if tokens[window[0]].token_type != TokenType::As || !is_identifier(&tokens[window[1]]) { + continue; + } + let normalized = tokens[window[1]].text.to_ascii_lowercase(); + if !seen.insert(normalized) { + duplicates.push(tokens[window[1]].span); + } + } + duplicates +} + +fn mixed_reference_clause_spans(tokens: &[Token], direct: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (position, &index) in direct.iter().enumerate() { + if !matches!( + tokens[index].token_type, + TokenType::Group | TokenType::Order + ) || direct + .get(position + 1) + .is_none_or(|&by| tokens[by].token_type != TokenType::By) + { + continue; + } + let end = direct[position + 2..] + .iter() + .position(|&candidate| is_clause_boundary(tokens[candidate].token_type)) + .map_or(direct.len(), |offset| position + 2 + offset); + let clause = &direct[position + 2..end]; + let mut has_number = false; + let mut has_name = false; + for (item_position, &candidate) in clause.iter().enumerate() { + if item_position == 0 + || tokens[clause[item_position - 1]].token_type == TokenType::Comma + { + has_number |= tokens[candidate].token_type == TokenType::Number; + has_name |= tokens[candidate].token_type != TokenType::Number; + } + } + if has_number && has_name { + spans.push(tokens[index].span); + } + } + spans +} + +fn is_clause_boundary(token_type: TokenType) -> bool { + matches!( + token_type, + TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + | TokenType::Fetch + | TokenType::Union + | TokenType::Intersect + | TokenType::Except + | TokenType::Semicolon + ) +} + +fn parenthesized_distinct_span( + tokens: &[Token], + depths: &[usize], + open_index: usize, +) -> Option { + let close_depth = depths[open_index] + 1; + let close_index = (open_index + 1..tokens.len()).find(|&index| { + tokens[index].token_type == TokenType::RParen && depths[index] == close_depth + })?; + Some(Span { + start: tokens[significant_before(tokens, open_index)?].span.start, + end: tokens[close_index].span.end, + line: tokens[significant_before(tokens, open_index)?].span.line, + column: tokens[significant_before(tokens, open_index)?].span.column, + }) +} + +fn count_one_span(tokens: &[Token], significant: &[usize], position: usize) -> Option { + let open = *significant.get(position + 1)?; + let one = *significant.get(position + 2)?; + let close = *significant.get(position + 3)?; + (tokens[open].token_type == TokenType::LParen + && tokens[one].token_type == TokenType::Number + && tokens[one].text == COUNT_ONE_LITERAL + && tokens[close].token_type == TokenType::RParen) + .then_some(tokens[one].span) +} + +fn simple_boolean_case_span( + tokens: &[Token], + significant: &[usize], + position: usize, +) -> Option { + let tail = &significant[position + 1..]; + let end_offset = tail + .iter() + .position(|&index| tokens[index].token_type == TokenType::End)?; + let body = &tail[..=end_offset]; + if body + .iter() + .any(|&index| tokens[index].token_type == TokenType::Case) + { + return None; + } + let when_position = body + .iter() + .position(|&index| tokens[index].token_type == TokenType::When)?; + let then_position = body + .iter() + .position(|&index| tokens[index].token_type == TokenType::Then)?; + let else_position = body + .iter() + .position(|&index| tokens[index].token_type == TokenType::Else)?; + let then_value = *body.get(then_position + 1)?; + let else_value = *body.get(else_position + 1)?; + if when_position != 0 + || tokens[then_value].token_type != TokenType::True + || tokens[else_value].token_type != TokenType::False + || else_position + 2 != body.len() - 1 + { + return None; + } + let start = tokens[significant[position]].span; + let end = tokens[*body.last()?].span; + Some(Span { + start: start.start, + end: end.end, + line: start.line, + column: start.column, + }) +} + +fn unstable_row_number_span( + tokens: &[Token], + depths: &[usize], + function_index: usize, +) -> Option { + let over_index = (function_index + 1..tokens.len()).find(|&index| { + depths[index] == depths[function_index] && tokens[index].token_type == TokenType::Over + })?; + let open_index = significant_after(tokens, over_index)?; + if tokens[open_index].token_type != TokenType::LParen { + return None; + } + let close_depth = depths[open_index] + 1; + let close_index = (open_index + 1..tokens.len()).find(|&index| { + tokens[index].token_type == TokenType::RParen && depths[index] == close_depth + })?; + (!tokens[open_index + 1..close_index] + .iter() + .any(|token| matches!(token.token_type, TokenType::Order | TokenType::OrderBy))) + .then_some(tokens[function_index].span) +} + +fn null_not_in_span( + tokens: &[Token], + depths: &[usize], + significant: &[usize], + position: usize, +) -> Option { + let in_index = *significant.get(position + 1)?; + let open_index = *significant.get(position + 2)?; + if tokens[in_index].token_type != TokenType::In + || tokens[open_index].token_type != TokenType::LParen + { + return None; + } + let close_depth = depths[open_index] + 1; + let close_index = (open_index + 1..tokens.len()).find(|&index| { + tokens[index].token_type == TokenType::RParen && depths[index] == close_depth + })?; + let list_depth = depths[open_index] + 1; + let list = &tokens[open_index + 1..close_index]; + (!list + .iter() + .any(|token| token.token_type == TokenType::Select) + && list.iter().enumerate().any(|(offset, token)| { + token.token_type == TokenType::Null && depths[open_index + 1 + offset] == list_depth + })) + .then_some(tokens[significant[position]].span) +} + +fn constant_predicate(tokens: &[Token], indices: &[usize]) -> bool { + let left = &tokens[indices[0]]; + let operator = &tokens[indices[1]]; + let right = &tokens[indices[2]]; + matches!( + left.token_type, + TokenType::Number | TokenType::String | TokenType::True | TokenType::False + ) && left.token_type == right.token_type + && left.text == right.text + && matches!(operator.token_type, TokenType::Eq | TokenType::Neq) +} + +fn set_arity_mismatch_spans(tokens: &[Token], depths: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if !matches!( + token.token_type, + TokenType::Union | TokenType::Intersect | TokenType::Except + ) { + continue; + } + let depth = depths[index]; + let Some(left_select) = (0..index).rev().find(|&candidate| { + depths[candidate] == depth && tokens[candidate].token_type == TokenType::Select + }) else { + continue; + }; + let Some(right_select) = (index + 1..tokens.len()).find(|&candidate| { + depths[candidate] == depth && tokens[candidate].token_type == TokenType::Select + }) else { + continue; + }; + let left_query = QuerySlice { + tokens, + depths, + direct: &[], + start: left_select, + end: index, + depth, + }; + let left_count = projection_arity(&left_query); + let right_end = query_end(tokens, depths, right_select, depth); + let right_query = QuerySlice { + tokens, + depths, + direct: &[], + start: right_select, + end: right_end, + depth, + }; + let right_count = projection_arity(&right_query); + if left_count + .zip(right_count) + .is_some_and(|(left, right)| left != right) + { + spans.push(token.span); + } + } + spans +} + +fn projection_arity(query: &QuerySlice<'_>) -> Option { + let QuerySlice { + tokens, + depths, + start: select, + end, + depth, + .. + } = query; + let select = *select; + let end = *end; + let depth = *depth; + let projection_end = (select + 1..end) + .find(|&index| depths[index] == depth && is_query_from(tokens, index)) + .unwrap_or(end); + if (select + 1..projection_end) + .any(|index| depths[index] == depth && tokens[index].token_type == TokenType::Star) + { + return None; + } + Some( + 1 + (select + 1..projection_end) + .filter(|&index| depths[index] == depth && tokens[index].token_type == TokenType::Comma) + .count(), + ) +} + +fn is_identifier(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::Identifier | TokenType::QuotedIdentifier | TokenType::Var + ) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/engine.rs b/crates/sqlclean/src/sql_lint/_helpers/engine.rs new file mode 100644 index 0000000..eb5212f --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/engine.rs @@ -0,0 +1,1978 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet, VecDeque}; + +use polyglot_sql::expressions::With; +use polyglot_sql::tokens::{Span, Token, TokenType}; +use polyglot_sql::{DialectType, Expression, ExpressionWalk}; + +use crate::sql_lint::constants::LINT_API_VERSION; +use crate::sql_lint::models::{ + AdditionalFactOptions, CrossJoinRequest, LintDiagnostic, LintEdit, LintRequest, LintResponse, + LintRuleMetadata, ParsedSql, QueryFacts, SqlMacroOutput, +}; + +use crate::sql_lint::_helpers::additional::collect_additional_facts; +use crate::sql_lint::_helpers::inline_relations::collect_inline_query_relation_spans; +use crate::sql_lint::_helpers::join_predicates::cross_join_diagnostics; +use crate::sql_lint::_helpers::parsing::parse; +use crate::sql_lint::_helpers::terminal_shape::collect_terminal_shape_facts; + +const NULL_COMPARISON: LintRuleMetadata = LintRuleMetadata { + code: "SQC001", + message: "Comparison with NULL is never true", + remediation: "Use IS NULL or IS NOT NULL when testing for NULL.", +}; +const CEREMONIAL_CTE_PREFIXES: [&str; 11] = [ + "__expected__", + "__assert__", + "__ref__", + "__source__", + "__seed__", + "__dbt_ref__", + "__macro_actual__", + "__macro_expected__", + "__udf_actual__", + "__udf_expected__", + "__table_fn_", +]; + +const IMPLICIT_CARTESIAN_JOIN: LintRuleMetadata = LintRuleMetadata { + code: "SQC002", + message: "Comma-separated sources create an implicit cartesian join", + remediation: "Replace comma-separated sources with an explicit keyed join, or use CROSS JOIN when the cartesian product is intentional.", +}; +const JOIN_WITHOUT_CONDITION: LintRuleMetadata = LintRuleMetadata { + code: "SQC003", + message: "Non-cross join has no meaningful condition", + remediation: "Add a meaningful ON or USING condition, or declare an intentional cartesian product with CROSS JOIN.", +}; +const UNORDERED_LIMIT: LintRuleMetadata = LintRuleMetadata { + code: "SQC004", + message: "Row selection is nondeterministic", + remediation: "Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET.", +}; +const UNUSED_CTE: LintRuleMetadata = LintRuleMetadata { + code: "SQC005", + message: "CTE is unreachable from the final query", + remediation: "Reference the CTE from the final query or another reachable CTE, or remove it.", +}; +const REDUNDANT_DISTINCT: LintRuleMetadata = LintRuleMetadata { + code: "SQC006", + message: "DISTINCT is redundant with the grouped output", + remediation: "Remove DISTINCT; the equivalent GROUP BY already determines the output groups.", +}; +const POSITIONAL_SET_STAR: LintRuleMetadata = LintRuleMetadata { + code: "SQC007", + message: "Positional set operation is vulnerable to column-order drift", + remediation: "Enumerate columns in the same order in every set-operation branch.", +}; +const EXPLICIT_UNION: LintRuleMetadata = LintRuleMetadata { + code: "SQC008", + message: "Bare UNION leaves duplicate handling implicit", + remediation: "Use UNION DISTINCT or UNION ALL to state duplicate handling explicitly.", +}; +const DUPLICATE_TABLE_ALIAS: LintRuleMetadata = LintRuleMetadata { + code: "SQC009", + message: "Table alias is duplicated in the same query scope", + remediation: "Give every relation in this query scope a unique alias.", +}; +const DUPLICATE_OUTPUT_ALIAS: LintRuleMetadata = LintRuleMetadata { + code: "SQC010", + message: "Output alias is duplicated in the same select list", + remediation: "Give every projected expression a unique output alias.", +}; +const MIXED_GROUP_ORDER_REFERENCES: LintRuleMetadata = LintRuleMetadata { + code: "SQC011", + message: "Clause mixes positional and named references", + remediation: "Use column names or positions consistently throughout this clause.", +}; +const REDUNDANT_ELSE_NULL: LintRuleMetadata = LintRuleMetadata { + code: "SQC012", + message: "ELSE NULL is redundant", + remediation: "Remove ELSE NULL; CASE expressions return NULL when ELSE is omitted.", +}; +const PARENTHESIZED_DISTINCT: LintRuleMetadata = LintRuleMetadata { + code: "SQC013", + message: "DISTINCT is written as though it were a function", + remediation: "Write DISTINCT as a SELECT modifier without function-like parentheses.", +}; +const CONSTANT_PREDICATE: LintRuleMetadata = LintRuleMetadata { + code: "SQC014", + message: "Predicate compares identical constant values", + remediation: "Remove the scaffold predicate or replace it with the intended condition.", +}; +const CONSECUTIVE_SEMICOLON: LintRuleMetadata = LintRuleMetadata { + code: "SQC015", + message: "Empty statement follows another terminator", + remediation: "Remove the redundant statement terminator.", +}; +const REDUNDANT_SELF_ALIAS: LintRuleMetadata = LintRuleMetadata { + code: "SQC016", + message: "Expression is aliased to its existing name", + remediation: "Remove the self-alias when the inferred output identity remains stable.", +}; +const COUNT_ONE: LintRuleMetadata = LintRuleMetadata { + code: "SQC017", + message: "Row count uses a constant expression", + remediation: "Use COUNT(*) to state that rows, rather than a nullable expression, are counted.", +}; +const UNSTABLE_ROW_NUMBER: LintRuleMetadata = LintRuleMetadata { + code: "SQC018", + message: "ROW_NUMBER has no deterministic ordering", + remediation: "Add ORDER BY with a stable tie-breaker inside the window specification.", +}; +const NULL_NOT_IN: LintRuleMetadata = LintRuleMetadata { + code: "SQC019", + message: "NOT IN list contains NULL and can reject every row", + remediation: "Remove NULL from the list or express the anti-join with a NULL-safe NOT EXISTS.", +}; +const SET_ARITY_MISMATCH: LintRuleMetadata = LintRuleMetadata { + code: "SQC020", + message: "Set-operation branches project different column counts", + remediation: "Project the same number of columns in every set-operation branch.", +}; +const PROJECTED_STAR: LintRuleMetadata = LintRuleMetadata { + code: "SQC021", + message: "Query output has an uncontrolled column shape", + remediation: "Enumerate the intended output columns explicitly.", +}; +const UNALIASED_CALCULATION: LintRuleMetadata = LintRuleMetadata { + code: "SQC022", + message: "Calculated projection has no stable output name", + remediation: "Add an explicit AS alias that describes the projected value.", +}; +const UNUSED_TABLE_ALIAS: LintRuleMetadata = LintRuleMetadata { + code: "SQC023", + message: "Table alias is never referenced", + remediation: "Use the alias to qualify references or remove it.", +}; +const NULL_REJECTED_LEFT_JOIN: LintRuleMetadata = LintRuleMetadata { + code: "SQC024", + message: "WHERE predicate rejects NULL rows from a LEFT JOIN", + remediation: "Move the right-side predicate into ON or use INNER JOIN when row rejection is intentional.", +}; +const IMPLICIT_INNER_JOIN: LintRuleMetadata = LintRuleMetadata { + code: "SQC025", + message: "JOIN type is implicit", + remediation: "Write INNER JOIN explicitly.", +}; +const AMBIGUOUS_ORDER_DIRECTIONS: LintRuleMetadata = LintRuleMetadata { + code: "SQC026", + message: "ORDER BY mixes explicit and implicit directions", + remediation: "State ASC or DESC for every ordering expression in this clause.", +}; +const UNQUALIFIED_MULTI_SOURCE_COLUMN: LintRuleMetadata = LintRuleMetadata { + code: "SQC027", + message: "Column reference is unqualified in a multi-relation query", + remediation: "Qualify the column with its relation alias.", +}; +const INCONSISTENT_SINGLE_SOURCE_QUALIFICATION: LintRuleMetadata = LintRuleMetadata { + code: "SQC028", + message: "Single-relation query mixes qualified and unqualified columns", + remediation: "Use one consistent qualification style in this query scope.", +}; +const UNKNOWN_RELATION_QUALIFIER: LintRuleMetadata = LintRuleMetadata { + code: "SQC029", + message: "Qualified reference uses a relation absent from this query scope", + remediation: "Use an available relation alias or add the intended relation to FROM.", +}; +const SIMPLE_BOOLEAN_CASE: LintRuleMetadata = LintRuleMetadata { + code: "SQC030", + message: "CASE expression only converts a condition to TRUE or FALSE", + remediation: "Use a NULL-safe boolean expression directly.", +}; +const NESTED_ELSE_CASE: LintRuleMetadata = LintRuleMetadata { + code: "SQC031", + message: "CASE expression is nested directly inside ELSE", + remediation: "Flatten the nested branch into the surrounding CASE expression.", +}; +const UNUSED_JOINED_RELATION: LintRuleMetadata = LintRuleMetadata { + code: "SQC032", + message: "Joined relation contributes no referenced values", + remediation: "Use the joined relation or remove the join. When the join intentionally filters rows, record it as a reviewed rule exception with a reason in the configuration file.", +}; +const COMMENT_ATTACHMENT: LintRuleMetadata = LintRuleMetadata { + code: "SQC033", + message: "Comment is not attached to the syntax node it explains", + remediation: "Use a standalone -- comment block immediately above the relevant CTE, JOIN, filter, predicate, selected expression, CASE branch, grouping, or ordering node.", +}; +const CTE_ONLY_BODY: LintRuleMetadata = LintRuleMetadata { + code: "SQC034", + message: "Query keeps transformation logic outside top-level CTEs", + remediation: "Move transformation logic into named top-level CTEs before the terminal SELECT.", +}; +const TERMINAL_SELECT: LintRuleMetadata = LintRuleMetadata { + code: "SQC035", + message: "Terminal SELECT does not read plainly from the final top-level CTE", + remediation: "Name the final logical CTE and leave one terminal SELECT from that CTE.", +}; +const NESTED_CTE: LintRuleMetadata = LintRuleMetadata { + code: "SQC036", + message: "CTE is nested inside another query", + remediation: "Hoist the nested CTE into the statement's top-level WITH list.", +}; +const RECURSIVE_CTE: LintRuleMetadata = LintRuleMetadata { + code: "SQC037", + message: "Recursive CTE is not permitted", + remediation: "Replace recursion with an explicit upstream relation or a bounded non-recursive query shape.", +}; +const CROSS_JOIN: LintRuleMetadata = LintRuleMetadata { + code: "SQC038", + message: "CROSS JOIN creates an explicit cartesian product", + remediation: "Use a keyed JOIN, or make one side provably at most one row (LIMIT 1, an aggregate without GROUP BY, or a one-row VALUES). When the cartesian product is intentional, record it as a reviewed [[rules.rule_exceptions]] entry with rule, path, relation and reason in the configuration file.", +}; +const INLINE_QUERY_RELATION: LintRuleMetadata = LintRuleMetadata { + code: "SQC039", + message: "Inline query relation obscures data flow", + remediation: "Extract the derived relation into a named top-level CTE; use explicit LATERAL only when the relation must remain correlated.", +}; +const TRIVIAL_EQUALITY_TOKEN_COUNT: usize = 3; +const JOIN_EXPRESSION: LintRuleMetadata = LintRuleMetadata { + code: "SQC040", + message: "JOIN ON contains a computed expression", + remediation: "Compute keys, including variant path access, in input CTEs and join on plain columns. Literal-only predicates such as ON TRUE are allowed.", +}; +const FINAL_CTE_NAME: LintRuleMetadata = LintRuleMetadata { + code: "SQC041", + message: "Only the last top-level CTE may be named final, and it must use that name", + remediation: "Name the last top-level CTE final and rename any other final CTE; SQC035 checks the terminal SELECT.", +}; + +pub(crate) const DEFAULT_RULES: [&str; 13] = [ + NULL_COMPARISON.code, + IMPLICIT_CARTESIAN_JOIN.code, + JOIN_WITHOUT_CONDITION.code, + UNORDERED_LIMIT.code, + UNUSED_CTE.code, + REDUNDANT_DISTINCT.code, + POSITIONAL_SET_STAR.code, + DUPLICATE_TABLE_ALIAS.code, + DUPLICATE_OUTPUT_ALIAS.code, + UNSTABLE_ROW_NUMBER.code, + NULL_NOT_IN.code, + SET_ARITY_MISMATCH.code, + INLINE_QUERY_RELATION.code, +]; + +const ALL_RULE_METADATA: [&LintRuleMetadata; 45] = [ + &NULL_COMPARISON, + &IMPLICIT_CARTESIAN_JOIN, + &JOIN_WITHOUT_CONDITION, + &UNORDERED_LIMIT, + &UNUSED_CTE, + &REDUNDANT_DISTINCT, + &POSITIONAL_SET_STAR, + &EXPLICIT_UNION, + &DUPLICATE_TABLE_ALIAS, + &DUPLICATE_OUTPUT_ALIAS, + &MIXED_GROUP_ORDER_REFERENCES, + &REDUNDANT_ELSE_NULL, + &PARENTHESIZED_DISTINCT, + &CONSTANT_PREDICATE, + &CONSECUTIVE_SEMICOLON, + &REDUNDANT_SELF_ALIAS, + &COUNT_ONE, + &UNSTABLE_ROW_NUMBER, + &NULL_NOT_IN, + &SET_ARITY_MISMATCH, + &PROJECTED_STAR, + &UNALIASED_CALCULATION, + &UNUSED_TABLE_ALIAS, + &NULL_REJECTED_LEFT_JOIN, + &IMPLICIT_INNER_JOIN, + &AMBIGUOUS_ORDER_DIRECTIONS, + &UNQUALIFIED_MULTI_SOURCE_COLUMN, + &INCONSISTENT_SINGLE_SOURCE_QUALIFICATION, + &UNKNOWN_RELATION_QUALIFIER, + &SIMPLE_BOOLEAN_CASE, + &NESTED_ELSE_CASE, + &UNUSED_JOINED_RELATION, + &COMMENT_ATTACHMENT, + &CTE_ONLY_BODY, + &TERMINAL_SELECT, + &NESTED_CTE, + &RECURSIVE_CTE, + &CROSS_JOIN, + &INLINE_QUERY_RELATION, + &JOIN_EXPRESSION, + &FINAL_CTE_NAME, + &crate::sql_quality::constants::UNUSED_CTE_OUTPUT, + &crate::sql_quality::constants::RANKING_SORT_CAP, + &crate::sql_quality::constants::LONG_LITERAL, + &crate::sql_quality::constants::REPEATED_JSON_PARSE, +]; + +fn is_ceremonial_cte_name(name: &str) -> bool { + CEREMONIAL_CTE_PREFIXES + .iter() + .any(|prefix| name.starts_with(prefix)) +} + +struct QueryTokenContext<'a> { + tokens: &'a [Token], + depths: &'a [usize], + direct: &'a [usize], + query_end: usize, + depth: usize, +} + +struct ExpressionRange { + start: usize, + end: usize, + depth: usize, + strip_alias: bool, +} + +struct DiagnosticContext<'a> { + sql: &'a str, + dialect: DialectType, + facts: &'a QueryFacts, + tokens: &'a [Token], + enabled: &'a HashSet, +} + +struct FactBuildOptions<'a> { + external_identifiers: &'a HashSet, + sql_macro_outputs: &'a [SqlMacroOutput], + dependency_identifiers: &'a HashSet, + externally_referenced_ctes: &'a HashSet, + allows_ceremonial_select: bool, + allows_dynamic_output_star: bool, + allows_empty_fixture_star: bool, +} + +pub(crate) fn lint_json_impl(request_json: &str) -> Result { + let request: LintRequest = + serde_json::from_str(request_json).map_err(|error| error.to_string())?; + let response = lint(request)?; + serde_json::to_string(&response).map_err(|error| error.to_string()) +} + +pub(crate) fn rule_metadata() -> Vec { + ALL_RULE_METADATA.iter().map(|rule| **rule).collect() +} + +pub(crate) fn lint(request: LintRequest) -> Result { + if request.version != LINT_API_VERSION { + return Err(format!( + "unsupported native lint request version {}; expected {LINT_API_VERSION}", + request.version + )); + } + let enabled = enabled_rules(request.enabled_rules, &request.ignored_rules)?; + let ParsedSql { + dialect: dialect_type, + tokens, + statements, + } = parse(&request.sql, &request.dialect)?; + let external_identifiers: HashSet = request + .external_identifiers + .iter() + .map(|identifier| identifier.to_ascii_lowercase()) + .collect(); + let dependency_identifiers: HashSet = request + .dependency_identifiers + .iter() + .map(|identifier| identifier.to_ascii_lowercase()) + .collect(); + let externally_referenced_ctes: HashSet = request + .externally_referenced_ctes + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect(); + let options = FactBuildOptions { + external_identifiers: &external_identifiers, + sql_macro_outputs: &request.sql_macro_outputs, + dependency_identifiers: &dependency_identifiers, + externally_referenced_ctes: &externally_referenced_ctes, + allows_ceremonial_select: request.allows_ceremonial_select, + allows_dynamic_output_star: request.allows_dynamic_output_star, + allows_empty_fixture_star: request.allows_empty_fixture_star, + }; + let facts = build_facts(&statements, &tokens, &request.sql, &options); + let context = DiagnosticContext { + sql: &request.sql, + dialect: dialect_type, + facts: &facts, + tokens: &tokens, + enabled: &enabled, + }; + let mut diagnostics = diagnostics(&context); + if enabled.contains(CROSS_JOIN.code) { + let cross_joins = CrossJoinRequest { + statements: &statements, + tokens: &tokens, + cross_spans: &facts.cross_joins, + relation_keys: &request.relation_keys, + relation_names: &request.relation_names, + }; + diagnostics.extend(cross_join_diagnostics(&cross_joins, &CROSS_JOIN)); + } + let depths = token_depths(&tokens); + if enabled.contains(JOIN_EXPRESSION.code) { + diagnostics.extend(diagnostics_for_spans( + &JOIN_EXPRESSION, + &crate::sql_lint::_helpers::join_predicates::join_expression_spans( + &tokens, + &depths, + dialect_type, + ), + Some("computing a join key requires an authored CTE rewrite"), + )); + } + if enabled.contains(FINAL_CTE_NAME.code) && !request.allows_ceremonial_select { + diagnostics.extend(diagnostics_for_spans( + &FINAL_CTE_NAME, + &crate::sql_lint::_helpers::terminal_shape::final_cte_name_spans(&tokens, &depths), + Some("renaming a CTE requires updating its references"), + )); + } + diagnostics.extend(crate::sql_quality::main::diagnostics::diagnostics( + &crate::sql_quality::models::QualityRequest { + statements: &statements, + tokens: &tokens, + dialect: dialect_type, + enabled: &enabled, + max_literal_length: request + .max_literal_length + .unwrap_or(crate::sql_quality::constants::DEFAULT_MAX_LITERAL_LENGTH), + max_ranking_order_by: request + .max_ranking_order_by + .unwrap_or(crate::sql_quality::constants::DEFAULT_MAX_RANKING_ORDER_BY), + relation_keys: &request.relation_keys, + externally_referenced_ctes: &externally_referenced_ctes, + fixture_body: request.allows_ceremonial_select, + config_file: request + .config_file + .as_deref() + .unwrap_or(crate::constants::DEFAULT_CONFIG_FILE), + }, + )?); + diagnostics.sort_by_key(|diagnostic| (diagnostic.start, diagnostic.end, diagnostic.code)); + Ok(LintResponse { + version: LINT_API_VERSION, + diagnostics, + }) +} + +fn enabled_rules( + requested: Option>, + ignored: &[String], +) -> Result, String> { + let selectors = + requested.unwrap_or_else(|| DEFAULT_RULES.iter().map(ToString::to_string).collect()); + let mut rules = expand_rule_selectors(&selectors)?; + for code in expand_rule_selectors(ignored)? { + rules.remove(&code); + } + Ok(rules) +} + +fn expand_rule_selectors(selectors: &[String]) -> Result, String> { + let mut selected: HashSet = HashSet::new(); + for selector in selectors { + let matches: Vec<&str> = ALL_RULE_METADATA + .iter() + .map(|rule| rule.code) + .filter(|code| code.starts_with(selector)) + .collect(); + if matches.is_empty() { + return Err(format!("unknown native lint rule '{selector}'")); + } + selected.extend(matches.into_iter().map(ToString::to_string)); + } + Ok(selected) +} + +fn build_facts( + expressions: &[Expression], + tokens: &[Token], + sql: &str, + options: &FactBuildOptions<'_>, +) -> QueryFacts { + let mut facts: QueryFacts = + collect_token_query_facts(tokens, sql, options.allows_ceremonial_select); + facts.null_comparisons = null_comparison_spans(tokens); + facts.additional = collect_additional_facts( + tokens, + &AdditionalFactOptions { + external_identifiers: options.external_identifiers, + sql_macro_outputs: options.sql_macro_outputs, + dependency_identifiers: options.dependency_identifiers, + allows_ceremonial_select: options.allows_ceremonial_select, + allows_dynamic_output_star: options.allows_dynamic_output_star, + allows_empty_fixture_star: options.allows_empty_fixture_star, + }, + ); + for expression in expressions { + facts + .unused_cte_names + .extend(collect_unused_ctes(expression, options)); + } + facts +} + +fn collect_token_query_facts( + tokens: &[Token], + sql: &str, + allows_ceremonial_select: bool, +) -> QueryFacts { + let depths = token_depths(tokens); + let mut facts = collect_rule_migration_facts(tokens, &depths, sql, allows_ceremonial_select); + for (select_index, token) in tokens.iter().enumerate() { + if token.token_type != TokenType::Select { + continue; + } + let depth = depths[select_index]; + let end = query_end(tokens, &depths, select_index, depth); + let direct = direct_indices(&depths, select_index + 1, end, depth); + let order = first_type(tokens, &direct, TokenType::Order); + let limit = first_row_selection_clause(tokens, &direct); + if order.is_none() + && let Some(index) = limit + && !limits_to_no_rows(tokens, index) + { + facts.unordered_limits.push(tokens[index].span); + } + let context = QueryTokenContext { + tokens, + depths: &depths, + direct: &direct, + query_end: end, + depth, + }; + if let Some(span) = redundant_distinct_span(&context) { + facts.redundant_distincts.push(span); + } + let (implicit, missing_conditions) = collect_from_and_join_facts(tokens, &direct); + facts.implicit_cartesian_joins.extend(implicit); + facts.joins_without_condition.extend(missing_conditions); + } + facts.positional_set_stars = collect_set_operation_facts(tokens, &depths); + facts +} + +fn collect_rule_migration_facts( + tokens: &[Token], + depths: &[usize], + sql: &str, + allows_ceremonial_select: bool, +) -> QueryFacts { + let mut facts = QueryFacts::default(); + let significant: Vec = (0..tokens.len()) + .filter(|&index| !is_layout(&tokens[index]) && !is_comment(&tokens[index])) + .collect(); + + let root_with = crate::sql_lint::_helpers::terminal_shape::root_with_indices(tokens, depths); + + for (position, &index) in significant.iter().enumerate() { + let text = tokens[index].text.as_str(); + if text.eq_ignore_ascii_case("cross") + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].token_type == TokenType::Join) + { + facts.cross_joins.push(tokens[index].span); + } + if tokens[index].token_type == TokenType::With { + let starts_hierarchy = position.checked_sub(1).is_some_and(|previous| { + tokens[significant[previous]].token_type == TokenType::Start + }); + if !root_with.contains(&index) && !starts_hierarchy { + facts.nested_ctes.push(tokens[index].span); + } + if !starts_hierarchy + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].text.eq_ignore_ascii_case("recursive")) + { + facts.recursive_ctes.push(tokens[index].span); + } + } + } + facts + .inline_query_relations + .extend(collect_inline_query_relation_spans( + tokens, + depths, + &significant, + )); + + let (cte_only_bodies, terminal_selects) = + collect_terminal_shape_facts(tokens, depths, &significant, allows_ceremonial_select); + facts.cte_only_bodies.extend(cte_only_bodies); + facts.terminal_selects.extend(terminal_selects); + facts + .invalid_comment_attachments + .extend(collect_comment_attachment_facts(tokens, sql)); + facts +} + +fn collect_comment_attachment_facts(tokens: &[Token], sql: &str) -> Vec { + let mut invalid: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if !token.trailing_comments.is_empty() { + invalid.push(token.span); + } + if token.comments.is_empty() { + continue; + } + let gap_start = index + .checked_sub(1) + .map_or(0, |previous| tokens[previous].span.end); + let gap = char_slice(sql, gap_start, token.span.start).unwrap_or_default(); + let block_comment = gap.contains("/*"); + let detached = gap.replace("\r\n", "\n").ends_with("\n\n"); + let unsupported_target = matches!( + token.token_type, + TokenType::Comma + | TokenType::RParen + | TokenType::Semicolon + | TokenType::Then + | TokenType::Else + | TokenType::From + ); + if block_comment || detached || unsupported_target { + invalid.push(token.span); + } + } + invalid +} + +pub(super) fn token_depths(tokens: &[Token]) -> Vec { + let mut depth = 0_usize; + tokens + .iter() + .map(|token| { + let current = depth; + if matches!( + token.token_type, + TokenType::LParen | TokenType::LBracket | TokenType::LBrace + ) { + depth += 1; + } else if matches!( + token.token_type, + TokenType::RParen | TokenType::RBracket | TokenType::RBrace + ) { + depth = depth.saturating_sub(1); + } + current + }) + .collect() +} + +pub(super) fn query_end(tokens: &[Token], depths: &[usize], start: usize, depth: usize) -> usize { + (start + 1..tokens.len()) + .find(|&index| { + depths[index] < depth + || depths[index] == depth + && matches!( + tokens[index].token_type, + TokenType::RParen + | TokenType::Semicolon + | TokenType::Union + | TokenType::Intersect + | TokenType::Except + ) + }) + .unwrap_or(tokens.len()) +} + +pub(super) fn direct_indices( + depths: &[usize], + start: usize, + end: usize, + depth: usize, +) -> Vec { + (start..end) + .filter(|&index| depths[index] == depth) + .collect() +} + +fn first_type(tokens: &[Token], indices: &[usize], token_type: TokenType) -> Option { + indices + .iter() + .copied() + .find(|&index| tokens[index].token_type == token_type) +} + +fn first_row_selection_clause(tokens: &[Token], direct: &[usize]) -> Option { + let from = direct + .iter() + .copied() + .find(|&index| is_query_from(tokens, index)); + direct.iter().copied().find(|&index| { + matches!( + tokens[index].token_type, + TokenType::Limit | TokenType::Offset + ) && from.is_none_or(|from_index| index > from_index) + && significant_before(tokens, index).is_none_or(|previous| { + !matches!( + tokens[previous].token_type, + TokenType::Colon | TokenType::DColon | TokenType::Dot | TokenType::DotColon + ) + }) + }) +} + +/// Whether a row-selection clause is `LIMIT 0`, which selects no rows in any order. +fn limits_to_no_rows(tokens: &[Token], index: usize) -> bool { + tokens[index].token_type == TokenType::Limit + && tokens.get(index + 1).is_some_and(|count| { + count.token_type == TokenType::Number && count.text.parse::() == Ok(0) + }) +} + +pub(super) fn is_query_from(tokens: &[Token], index: usize) -> bool { + tokens[index].token_type == TokenType::From + && significant_before(tokens, index) + .is_none_or(|previous| tokens[previous].token_type != TokenType::Distinct) +} + +fn redundant_distinct_span(context: &QueryTokenContext<'_>) -> Option { + let QueryTokenContext { + tokens, + depths, + direct, + query_end, + depth, + } = context; + let group_position = group_by_position(tokens, direct)?; + let group_index = direct[group_position]; + let projection_end = direct + .iter() + .copied() + .find(|&index| is_query_from(tokens, index)) + .filter(|index| *index < group_index) + .unwrap_or(group_index); + let distinct_position = direct.iter().position(|&index| { + index < projection_end && tokens[index].token_type == TokenType::Distinct + })?; + if direct + .get(distinct_position + 1) + .is_some_and(|&index| tokens[index].token_type == TokenType::On) + { + return None; + } + let projection_start = direct[distinct_position] + 1; + let group_start = *direct.get(group_position + 2)?; + let group_end = direct[group_position + 2..] + .iter() + .copied() + .find(|&index| { + matches!( + tokens[index].token_type, + TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + ) + }) + .unwrap_or(*query_end); + let projection_signatures = expression_signatures( + tokens, + depths, + &ExpressionRange { + start: projection_start, + end: projection_end, + depth: *depth, + strip_alias: true, + }, + ); + let group_signatures = expression_signatures( + tokens, + depths, + &ExpressionRange { + start: group_start, + end: group_end, + depth: *depth, + strip_alias: false, + }, + ); + if group_signatures.is_empty() + || !group_signatures + .iter() + .all(|group| projection_signatures.contains(group)) + { + return None; + } + Some(tokens[direct[distinct_position]].span) +} + +fn group_by_position(tokens: &[Token], direct: &[usize]) -> Option { + direct.windows(2).position(|window| { + tokens[window[0]].token_type == TokenType::Group + && tokens[window[1]].text.eq_ignore_ascii_case("BY") + }) +} + +fn expression_signatures( + tokens: &[Token], + depths: &[usize], + range: &ExpressionRange, +) -> Vec { + let mut boundaries: Vec = (range.start..range.end) + .filter(|&index| { + depths[index] == range.depth && tokens[index].token_type == TokenType::Comma + }) + .collect(); + boundaries.push(range.end); + let mut segment_start = range.start; + let mut signatures: Vec = Vec::new(); + for segment_end in boundaries { + let effective_end = alias_start(tokens, depths, range, segment_start..segment_end); + let parts: Vec = tokens[segment_start..effective_end] + .iter() + .map(|token| token.text.to_ascii_lowercase()) + .collect(); + let signature = parts.join(" "); + if !signature.is_empty() { + signatures.push(signature); + } + segment_start = segment_end + 1; + } + signatures +} + +fn alias_start( + tokens: &[Token], + depths: &[usize], + range: &ExpressionRange, + segment: std::ops::Range, +) -> usize { + if !range.strip_alias { + return segment.end; + } + segment + .clone() + .find(|&index| depths[index] == range.depth && tokens[index].token_type == TokenType::As) + .unwrap_or(segment.end) +} + +fn collect_from_and_join_facts(tokens: &[Token], direct: &[usize]) -> (Vec, Vec) { + let mut implicit: Vec = Vec::new(); + let mut missing_conditions: Vec = Vec::new(); + let Some(from_position) = direct + .iter() + .position(|&index| is_query_from(tokens, index)) + else { + return (implicit, missing_conditions); + }; + let clause_end = direct[from_position + 1..] + .iter() + .position(|&index| is_post_from_clause(tokens[index].token_type)) + .map_or(direct.len(), |offset| from_position + 1 + offset); + let relation_clause = &direct[from_position + 1..clause_end]; + let values_relation = relation_clause + .first() + .is_some_and(|&index| tokens[index].token_type == TokenType::Values); + for (position, &index) in relation_clause.iter().enumerate() { + if tokens[index].token_type == TokenType::Comma + && !values_relation + && relation_clause + .get(position + 1) + .is_none_or(|&next| tokens[next].token_type != TokenType::Lateral) + { + implicit.push(tokens[index].span); + } + } + for (position, &index) in direct.iter().enumerate() { + if tokens[index].token_type != TokenType::Join + || is_conditionless_join(tokens, direct, position) + { + continue; + } + let condition_end = direct[position + 1..] + .iter() + .position(|&candidate| { + tokens[candidate].token_type == TokenType::Join + || is_post_from_clause(tokens[candidate].token_type) + }) + .map_or(direct.len(), |offset| position + 1 + offset); + let condition = &direct[position + 1..condition_end]; + let on_position = condition + .iter() + .position(|&candidate| tokens[candidate].token_type == TokenType::On); + let has_using = condition + .iter() + .any(|&candidate| tokens[candidate].token_type == TokenType::Using); + if !has_using + && on_position + .is_none_or(|offset| trivial_on_condition(tokens, &condition[offset + 1..])) + { + missing_conditions.push(tokens[index].span); + } + } + (implicit, missing_conditions) +} + +fn is_post_from_clause(token_type: TokenType) -> bool { + matches!( + token_type, + TokenType::Join + | TokenType::Where + | TokenType::Group + | TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + | TokenType::Fetch + ) +} + +fn is_conditionless_join(tokens: &[Token], direct: &[usize], join_position: usize) -> bool { + direct[..join_position] + .iter() + .rev() + .take_while(|&&index| { + !matches!( + tokens[index].token_type, + TokenType::From | TokenType::Join | TokenType::Comma + ) + }) + .any(|&index| { + matches!( + tokens[index].text.to_ascii_uppercase().as_str(), + "CROSS" | "NATURAL" | "APPLY" | "POSITIONAL" | "PASTE" + ) + }) +} + +fn trivial_on_condition(tokens: &[Token], condition: &[usize]) -> bool { + let significant: Vec<&Token> = condition + .iter() + .map(|&index| &tokens[index]) + .filter(|token| !matches!(token.token_type, TokenType::LParen | TokenType::RParen)) + .collect(); + significant.is_empty() + || significant.len() == 1 && significant[0].token_type == TokenType::True + || significant.len() == TRIVIAL_EQUALITY_TOKEN_COUNT + && significant[1].token_type == TokenType::Eq + && matches!( + significant[0].token_type, + TokenType::Number | TokenType::String | TokenType::True | TokenType::False + ) + && significant[0].token_type == significant[2].token_type + && significant[0].text == significant[2].text +} + +fn collect_set_operation_facts(tokens: &[Token], depths: &[usize]) -> Vec { + let mut spans: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if !matches!( + token.token_type, + TokenType::Union | TokenType::Intersect | TokenType::Except + ) { + continue; + } + let depth = depths[index]; + let by_name = tokens[index + 1..] + .iter() + .take_while(|candidate| candidate.token_type != TokenType::Select) + .any(|candidate| candidate.text.eq_ignore_ascii_case("BY")) + && tokens[index + 1..] + .iter() + .take_while(|candidate| candidate.token_type != TokenType::Select) + .any(|candidate| candidate.text.eq_ignore_ascii_case("NAME")); + if !by_name + && (select_before_projects_star(tokens, depths, index, depth) + || select_after_projects_star(tokens, depths, index, depth)) + { + spans.push(token.span); + } + } + spans +} + +fn select_before_projects_star( + tokens: &[Token], + depths: &[usize], + end: usize, + depth: usize, +) -> bool { + let Some(start) = (0..end) + .rev() + .find(|&index| depths[index] == depth && tokens[index].token_type == TokenType::Select) + .or_else(|| { + (0..end) + .rev() + .filter(|&index| tokens[index].token_type == TokenType::Select) + .min_by_key(|&index| depths[index]) + }) + else { + return false; + }; + projection_projects_star( + tokens, + depths, + &ExpressionRange { + start, + end, + depth: depths[start], + strip_alias: false, + }, + ) +} + +fn select_after_projects_star( + tokens: &[Token], + depths: &[usize], + start: usize, + depth: usize, +) -> bool { + let Some(select) = (start + 1..tokens.len()) + .find(|&index| depths[index] == depth && tokens[index].token_type == TokenType::Select) + .or_else(|| { + (start + 1..tokens.len()) + .filter(|&index| tokens[index].token_type == TokenType::Select) + .min_by_key(|&index| depths[index]) + }) + else { + return false; + }; + let select_depth = depths[select]; + projection_projects_star( + tokens, + depths, + &ExpressionRange { + start: select, + end: query_end(tokens, depths, select, select_depth), + depth: select_depth, + strip_alias: false, + }, + ) +} + +fn projection_projects_star(tokens: &[Token], depths: &[usize], range: &ExpressionRange) -> bool { + (range.start + 1..range.end) + .take_while(|&index| depths[index] != range.depth || !is_query_from(tokens, index)) + .any(|index| depths[index] == range.depth && tokens[index].token_type == TokenType::Star) +} + +fn collect_unused_ctes(expression: &Expression, options: &FactBuildOptions<'_>) -> Vec { + let mut unused: Vec = Vec::new(); + for node in expression.dfs() { + match node { + Expression::Select(select) if select.with.is_some() => { + let mut root = select.clone(); + if let Some(with) = root.with.take() { + unused.extend(collect_unused_for_with( + &with, + &Expression::Select(root), + options, + )); + } + } + Expression::Union(operation) if operation.with.is_some() => { + let mut root = operation.clone(); + if let Some(with) = root.with.take() { + unused.extend(collect_unused_for_with( + &with, + &Expression::Union(root), + options, + )); + } + } + Expression::Intersect(operation) if operation.with.is_some() => { + let mut root = operation.clone(); + if let Some(with) = root.with.take() { + unused.extend(collect_unused_for_with( + &with, + &Expression::Intersect(root), + options, + )); + } + } + Expression::Except(operation) if operation.with.is_some() => { + let mut root = operation.clone(); + if let Some(with) = root.with.take() { + unused.extend(collect_unused_for_with( + &with, + &Expression::Except(root), + options, + )); + } + } + _ => {} + }; + } + unused +} + +fn collect_unused_for_with( + with: &With, + root: &Expression, + options: &FactBuildOptions<'_>, +) -> Vec { + let names: HashSet = with + .ctes + .iter() + .map(|cte| cte.alias.name.to_ascii_lowercase()) + .collect(); + let dependencies: HashMap> = with + .ctes + .iter() + .map(|cte| { + ( + cte.alias.name.to_ascii_lowercase(), + table_names(&cte.this) + .intersection(&names) + .cloned() + .collect(), + ) + }) + .collect(); + let mut reachable: HashSet = table_names(root).intersection(&names).cloned().collect(); + reachable.extend( + options + .externally_referenced_ctes + .intersection(&names) + .cloned(), + ); + if options.allows_ceremonial_select { + reachable.extend( + names + .iter() + .filter(|name| is_ceremonial_cte_name(name)) + .cloned(), + ); + } + let mut queue: VecDeque = reachable.iter().cloned().collect(); + while let Some(name) = queue.pop_front() { + if let Some(required) = dependencies.get(&name) { + for dependency in required { + if reachable.insert(dependency.clone()) { + queue.push_back(dependency.clone()); + } + } + } + } + with.ctes + .iter() + .filter(|cte| !reachable.contains(&cte.alias.name.to_ascii_lowercase())) + .map(|cte| cte.alias.name.clone()) + .collect() +} + +fn table_names(expression: &Expression) -> HashSet { + expression + .dfs() + .filter_map(|node| match node { + Expression::Table(table) => Some(table.name.name.to_ascii_lowercase()), + _ => None, + }) + .collect() +} + +fn null_comparison_fix(tokens: &[Token], span: Span) -> Option { + let index = token_index_for_span(tokens, span)?; + let token = &tokens[index]; + if !adjacent_null(tokens, index, true) { + return None; + } + let replacement = match token.token_type { + TokenType::Eq => "IS", + TokenType::Neq => "IS NOT", + _ => return None, + }; + Some(LintEdit { + start: span.start, + end: span.end, + replacement: replacement.to_string(), + }) +} + +fn redundant_distinct_fix(tokens: &[Token], span: Span) -> LintEdit { + let end = token_index_for_span(tokens, span) + .and_then(|index| significant_after(tokens, index)) + .filter(|&index| !is_comment(&tokens[index])) + .map_or(span.end, |index| tokens[index].span.start); + LintEdit { + start: span.start, + end, + replacement: String::new(), + } +} + +fn implicit_cartesian_fix(sql: &str, tokens: &[Token], span: Span) -> Option { + let comma_index = token_index_for_span(tokens, span)?; + let depths = token_depths(tokens); + let depth = depths[comma_index]; + let from_index = (0..comma_index).rev().find(|&index| { + depths[index] == depth + && (is_query_from(tokens, index) || tokens[index].token_type == TokenType::Semicolon) + })?; + if !is_query_from(tokens, from_index) { + return None; + } + let clause_end = (comma_index + 1..tokens.len()) + .find(|&index| depths[index] == depth && is_post_from_clause(tokens[index].token_type)) + .unwrap_or(tokens.len()); + if tokens[from_index + 1..clause_end] + .iter() + .any(|token| token.token_type == TokenType::Join) + { + return None; + } + let next_index = significant_after(tokens, comma_index)?; + let end = tokens[next_index].span.start; + let replaced = char_slice(sql, span.start, end)?; + if contains_comment(replaced) { + return None; + } + Some(LintEdit { + start: span.start, + end, + replacement: " CROSS JOIN ".to_string(), + }) +} + +fn conditionless_join_fix(tokens: &[Token], span: Span) -> Option { + let join_index = token_index_for_span(tokens, span)?; + let depths = token_depths(tokens); + let depth = depths[join_index]; + if significant_before(tokens, join_index).is_some_and(|index| { + matches!( + tokens[index].text.to_ascii_uppercase().as_str(), + "INNER" + | "LEFT" + | "RIGHT" + | "FULL" + | "OUTER" + | "CROSS" + | "NATURAL" + | "ASOF" + | "SEMI" + | "ANTI" + ) + }) { + return None; + } + let condition_end = (join_index + 1..tokens.len()) + .find(|&index| { + depths[index] == depth + && (tokens[index].token_type == TokenType::Join + || is_post_from_clause(tokens[index].token_type)) + }) + .unwrap_or(tokens.len()); + if tokens[join_index + 1..condition_end] + .iter() + .any(|token| matches!(token.token_type, TokenType::On | TokenType::Using)) + { + return None; + } + Some(LintEdit { + start: span.start, + end: span.end, + replacement: "CROSS JOIN".to_string(), + }) +} + +fn unused_cte_fix(sql: &str, tokens: &[Token], name_span: Span) -> Option { + let name_index = token_index_for_span(tokens, name_span)?; + if tokens[name_index].text.starts_with("__") { + return None; + } + let as_index = significant_after(tokens, name_index)?; + if tokens[as_index].token_type != TokenType::As { + return None; + } + let open_index = significant_after(tokens, as_index)?; + if tokens[open_index].token_type != TokenType::LParen { + return None; + } + let depths = token_depths(tokens); + let close_depth = depths[open_index] + 1; + let close_index = (open_index + 1..tokens.len()).find(|&index| { + tokens[index].token_type == TokenType::RParen && depths[index] == close_depth + })?; + let body_tokens = &tokens[open_index + 1..close_index]; + let first_body = body_tokens.iter().find(|token| !is_layout(token))?; + if !matches!(first_body.token_type, TokenType::Select | TokenType::With) + || body_tokens.iter().any(|token| { + matches!( + token.text.to_ascii_uppercase().as_str(), + "INSERT" | "UPDATE" | "DELETE" | "MERGE" + ) + }) + { + return None; + } + let previous_index = significant_before(tokens, name_index)?; + let next_index = significant_after(tokens, close_index)?; + let (start, end) = if tokens[next_index].token_type == TokenType::Comma { + let following_index = significant_after(tokens, next_index)?; + (name_span.start, tokens[following_index].span.start) + } else if tokens[previous_index].token_type == TokenType::Comma { + ( + tokens[previous_index].span.start, + tokens[close_index].span.end, + ) + } else if tokens[previous_index].token_type == TokenType::With { + ( + tokens[previous_index].span.start, + tokens[next_index].span.start, + ) + } else { + return None; + }; + if contains_comment(char_slice(sql, start, end)?) { + return None; + } + Some(LintEdit { + start, + end, + replacement: String::new(), + }) +} + +fn token_index_for_span(tokens: &[Token], span: Span) -> Option { + tokens.iter().position(|token| token.span == span) +} + +pub(super) fn significant_before(tokens: &[Token], index: usize) -> Option { + (0..index) + .rev() + .find(|&candidate| !is_layout(&tokens[candidate])) +} + +pub(super) fn significant_after(tokens: &[Token], index: usize) -> Option { + (index + 1..tokens.len()).find(|&candidate| !is_layout(&tokens[candidate])) +} + +pub(super) fn is_layout(token: &Token) -> bool { + matches!(token.token_type, TokenType::Space | TokenType::Break) +} + +pub(super) fn is_comment(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::LineComment | TokenType::BlockComment + ) +} + +fn diagnostics(context: &DiagnosticContext<'_>) -> Vec { + let mut output: Vec = core_diagnostics(context); + output.extend(additional_style_diagnostics(context)); + output.extend(additional_relation_diagnostics(context)); + output.extend(rule_migration_diagnostics(context)); + output +} + +fn core_diagnostics(context: &DiagnosticContext<'_>) -> Vec { + let DiagnosticContext { + sql, + dialect, + facts, + tokens, + enabled, + } = context; + let mut diagnostics: Vec = Vec::new(); + if enabled.contains(NULL_COMPARISON.code) { + diagnostics.extend(facts.null_comparisons.iter().map(|span| { + let fix = null_comparison_fix(tokens, *span); + diagnostic( + &NULL_COMPARISON, + Some(*span), + fix, + Some("only equality and inequality comparisons have a deterministic repair"), + ) + })); + } + if enabled.contains(IMPLICIT_CARTESIAN_JOIN.code) { + diagnostics.extend(facts.implicit_cartesian_joins.iter().map(|span| { + let fix = implicit_cartesian_fix(sql, tokens, *span); + diagnostic( + &IMPLICIT_CARTESIAN_JOIN, + Some(*span), + fix, + Some("mixed joins, comments, or generated SQL require an explicit authored repair"), + ) + })); + } + if enabled.contains(JOIN_WITHOUT_CONDITION.code) { + diagnostics.extend(facts.joins_without_condition.iter().map(|span| { + let fix = conditionless_join_fix(tokens, *span); + diagnostic( + &JOIN_WITHOUT_CONDITION, + Some(*span), + fix, + Some("qualified joins or joins with placeholder conditions require user intent"), + ) + })); + } + if enabled.contains(UNORDERED_LIMIT.code) { + diagnostics.extend(diagnostics_for_spans( + &UNORDERED_LIMIT, + &facts.unordered_limits, + Some("deterministic ordering columns and tie-breakers require user intent"), + )); + } + if enabled.contains(UNUSED_CTE.code) { + for span in unused_cte_spans(tokens, &facts.unused_cte_names) { + let fix = unused_cte_fix(sql, tokens, span); + diagnostics.push(diagnostic( + &UNUSED_CTE, + Some(span), + fix, + Some("only contiguous authored, comment-free, select-only CTEs can be removed"), + )); + } + } + if enabled.contains(REDUNDANT_DISTINCT.code) { + diagnostics.extend(facts.redundant_distincts.iter().map(|span| { + diagnostic( + &REDUNDANT_DISTINCT, + Some(*span), + Some(redundant_distinct_fix(tokens, *span)), + None, + ) + })); + } + if enabled.contains(POSITIONAL_SET_STAR.code) { + diagnostics.extend(diagnostics_for_spans( + &POSITIONAL_SET_STAR, + &facts.positional_set_stars, + Some( + "column names and their intended positional order require resolved schema evidence", + ), + )); + } + if enabled.contains(EXPLICIT_UNION.code) { + diagnostics.extend(facts.additional.bare_unions.iter().map(|span| { + let fix = explicit_union_fix(*dialect, *span); + diagnostic( + &EXPLICIT_UNION, + Some(*span), + fix, + Some("the active dialect does not accept explicit UNION DISTINCT"), + ) + })); + } + diagnostics +} + +fn additional_style_diagnostics(context: &DiagnosticContext<'_>) -> Vec { + let DiagnosticContext { + sql, + facts, + enabled, + .. + } = context; + let mut diagnostics: Vec = Vec::new(); + if enabled.contains(DUPLICATE_TABLE_ALIAS.code) { + diagnostics.extend(diagnostics_for_spans( + &DUPLICATE_TABLE_ALIAS, + &facts.additional.duplicate_table_aliases, + Some("choosing a new relation alias requires user intent"), + )); + } + if enabled.contains(DUPLICATE_OUTPUT_ALIAS.code) { + diagnostics.extend(diagnostics_for_spans( + &DUPLICATE_OUTPUT_ALIAS, + &facts.additional.duplicate_output_aliases, + Some("choosing a stable output name requires user intent"), + )); + } + if enabled.contains(MIXED_GROUP_ORDER_REFERENCES.code) { + diagnostics.extend(diagnostics_for_spans( + &MIXED_GROUP_ORDER_REFERENCES, + &facts.additional.mixed_group_order_references, + Some("normalization requires reliable projection-name resolution"), + )); + } + if enabled.contains(REDUNDANT_ELSE_NULL.code) { + diagnostics.extend(facts.additional.redundant_else_nulls.iter().map(|span| { + diagnostic( + &REDUNDANT_ELSE_NULL, + Some(*span), + Some(deletion(*span)), + None, + ) + })); + } + if enabled.contains(PARENTHESIZED_DISTINCT.code) { + diagnostics.extend(facts.additional.parenthesized_distincts.iter().map(|span| { + diagnostic( + &PARENTHESIZED_DISTINCT, + Some(*span), + parenthesized_distinct_fix(sql, *span), + Some("comments or malformed parentheses prevent a lossless rewrite"), + ) + })); + } + if enabled.contains(CONSTANT_PREDICATE.code) { + diagnostics.extend(diagnostics_for_spans( + &CONSTANT_PREDICATE, + &facts.additional.constant_predicates, + Some("removing or replacing a scaffold predicate requires user intent"), + )); + } + if enabled.contains(CONSECUTIVE_SEMICOLON.code) { + diagnostics.extend(facts.additional.consecutive_semicolons.iter().map(|span| { + diagnostic( + &CONSECUTIVE_SEMICOLON, + Some(*span), + Some(deletion(*span)), + None, + ) + })); + } + if enabled.contains(REDUNDANT_SELF_ALIAS.code) { + diagnostics.extend(facts.additional.redundant_self_aliases.iter().map(|span| { + diagnostic( + &REDUNDANT_SELF_ALIAS, + Some(*span), + None, + Some("removing the alias may change statically inferred output identity"), + ) + })); + } + if enabled.contains(COUNT_ONE.code) { + diagnostics.extend(facts.additional.count_one_literals.iter().map(|span| { + diagnostic( + &COUNT_ONE, + Some(*span), + Some(LintEdit { + start: span.start, + end: span.end, + replacement: "*".to_string(), + }), + None, + ) + })); + } + if enabled.contains(UNSTABLE_ROW_NUMBER.code) { + diagnostics.extend(diagnostics_for_spans( + &UNSTABLE_ROW_NUMBER, + &facts.additional.unstable_row_numbers, + Some("ordering columns and a stable tie-breaker require user intent"), + )); + } + diagnostics +} + +fn additional_relation_diagnostics(context: &DiagnosticContext<'_>) -> Vec { + let DiagnosticContext { + sql, + facts, + tokens, + enabled, + .. + } = context; + let mut diagnostics: Vec = Vec::new(); + if enabled.contains(NULL_NOT_IN.code) { + diagnostics.extend(diagnostics_for_spans( + &NULL_NOT_IN, + &facts.additional.null_not_in_predicates, + Some("the intended NULL and anti-join semantics require user intent"), + )); + } + if enabled.contains(SET_ARITY_MISMATCH.code) { + diagnostics.extend(diagnostics_for_spans( + &SET_ARITY_MISMATCH, + &facts.additional.set_arity_mismatches, + Some("the intended branch schema requires user intent"), + )); + } + if enabled.contains(PROJECTED_STAR.code) { + diagnostics.extend(diagnostics_for_spans( + &PROJECTED_STAR, + &facts.additional.projected_stars, + Some("column enumeration requires reliable resolved schema evidence"), + )); + } + if enabled.contains(UNALIASED_CALCULATION.code) { + diagnostics.extend(diagnostics_for_spans( + &UNALIASED_CALCULATION, + &facts.additional.unaliased_calculations, + Some("a meaningful stable output name requires user intent"), + )); + } + if enabled.contains(UNUSED_TABLE_ALIAS.code) { + diagnostics.extend(facts.additional.unused_table_aliases.iter().map(|span| { + diagnostic( + &UNUSED_TABLE_ALIAS, + Some(*span), + Some(deletion(*span)), + None, + ) + })); + } + if enabled.contains(NULL_REJECTED_LEFT_JOIN.code) { + diagnostics.extend(diagnostics_for_spans( + &NULL_REJECTED_LEFT_JOIN, + &facts.additional.null_rejected_left_joins, + Some("moving the predicate or changing join type requires user intent"), + )); + } + if enabled.contains(IMPLICIT_INNER_JOIN.code) { + diagnostics.extend(facts.additional.implicit_inner_joins.iter().map(|span| { + diagnostic( + &IMPLICIT_INNER_JOIN, + Some(*span), + Some(LintEdit { + start: span.start, + end: span.end, + replacement: "INNER JOIN".to_string(), + }), + None, + ) + })); + } + if enabled.contains(AMBIGUOUS_ORDER_DIRECTIONS.code) { + diagnostics.extend(diagnostics_for_spans( + &AMBIGUOUS_ORDER_DIRECTIONS, + &facts.additional.ambiguous_order_directions, + Some("normalizing every expression requires a multi-range authored rewrite"), + )); + } + if enabled.contains(UNQUALIFIED_MULTI_SOURCE_COLUMN.code) { + diagnostics.extend(diagnostics_for_spans( + &UNQUALIFIED_MULTI_SOURCE_COLUMN, + &facts.additional.unqualified_multi_source_columns, + Some("choosing the correct relation qualifier requires user intent"), + )); + } + if enabled.contains(INCONSISTENT_SINGLE_SOURCE_QUALIFICATION.code) { + diagnostics.extend(diagnostics_for_spans( + &INCONSISTENT_SINGLE_SOURCE_QUALIFICATION, + &facts.additional.inconsistent_single_source_qualification, + Some("qualification style is normalized by the canonical formatter only when safe"), + )); + } + if enabled.contains(UNKNOWN_RELATION_QUALIFIER.code) { + diagnostics.extend(diagnostics_for_spans( + &UNKNOWN_RELATION_QUALIFIER, + &facts.additional.unknown_relation_qualifiers, + Some("the intended relation or alias requires user intent"), + )); + } + if enabled.contains(SIMPLE_BOOLEAN_CASE.code) { + diagnostics.extend(facts.additional.simple_boolean_cases.iter().map(|span| { + diagnostic( + &SIMPLE_BOOLEAN_CASE, + Some(*span), + simple_boolean_case_fix(sql, tokens, *span), + Some("comments or a non-canonical CASE shape prevent a lossless rewrite"), + ) + })); + } + if enabled.contains(NESTED_ELSE_CASE.code) { + diagnostics.extend(diagnostics_for_spans( + &NESTED_ELSE_CASE, + &facts.additional.nested_else_cases, + Some("flattening nested branches requires a multi-range authored rewrite"), + )); + } + if enabled.contains(UNUSED_JOINED_RELATION.code) { + diagnostics.extend(diagnostics_for_spans( + &UNUSED_JOINED_RELATION, + &facts.additional.unused_joined_relations, + Some("the join may intentionally filter or multiply rows"), + )); + } + diagnostics +} + +fn rule_migration_diagnostics(context: &DiagnosticContext<'_>) -> Vec { + let DiagnosticContext { facts, enabled, .. } = context; + let mut diagnostics: Vec = Vec::new(); + if enabled.contains(COMMENT_ATTACHMENT.code) { + diagnostics.extend(diagnostics_for_spans( + &COMMENT_ATTACHMENT, + &facts.invalid_comment_attachments, + Some("comment attachment requires an authored placement decision"), + )); + } + if enabled.contains(CTE_ONLY_BODY.code) { + diagnostics.extend(diagnostics_for_spans( + &CTE_ONLY_BODY, + &facts.cte_only_bodies, + Some("extracting transformation logic into named CTEs requires user intent"), + )); + } + if enabled.contains(TERMINAL_SELECT.code) { + diagnostics.extend(diagnostics_for_spans( + &TERMINAL_SELECT, + &facts.terminal_selects, + Some("choosing the final logical CTE and projection requires user intent"), + )); + } + if enabled.contains(NESTED_CTE.code) { + diagnostics.extend(diagnostics_for_spans( + &NESTED_CTE, + &facts.nested_ctes, + Some("hoisting a nested CTE requires an authored query rewrite"), + )); + } + if enabled.contains(RECURSIVE_CTE.code) { + diagnostics.extend(diagnostics_for_spans( + &RECURSIVE_CTE, + &facts.recursive_ctes, + Some("replacing recursion requires an authored query design"), + )); + } + if enabled.contains(INLINE_QUERY_RELATION.code) { + diagnostics.extend(diagnostics_for_spans( + &INLINE_QUERY_RELATION, + &facts.inline_query_relations, + Some("extracting a derived relation can change correlation and scope"), + )); + } + diagnostics +} + +fn deletion(span: Span) -> LintEdit { + LintEdit { + start: span.start, + end: span.end, + replacement: String::new(), + } +} + +fn parenthesized_distinct_fix(sql: &str, span: Span) -> Option { + let source = char_slice(sql, span.start, span.end)?; + if contains_comment(source) { + return None; + } + let open = source.find('(')?; + let close = source.rfind(')')?; + (open < close).then(|| LintEdit { + start: span.start, + end: span.end, + replacement: format!("DISTINCT {}", &source[open + 1..close]), + }) +} + +fn simple_boolean_case_fix(sql: &str, tokens: &[Token], span: Span) -> Option { + let case_index = token_index_for_span(tokens, span).or_else(|| { + tokens + .iter() + .position(|token| token.span.start == span.start && token.token_type == TokenType::Case) + })?; + let end_index = tokens.iter().position(|token| token.span.end == span.end)?; + let source = char_slice(sql, span.start, span.end)?; + if contains_comment(source) { + return None; + } + let case_tokens = &tokens[case_index..=end_index]; + if case_tokens + .iter() + .filter(|token| token.token_type == TokenType::When) + .count() + != 1 + || case_tokens + .iter() + .filter(|token| token.token_type == TokenType::Then) + .count() + != 1 + || case_tokens + .iter() + .filter(|token| token.token_type == TokenType::Else) + .count() + != 1 + { + return None; + } + let when_index = + (case_index + 1..end_index).find(|&index| tokens[index].token_type == TokenType::When)?; + let then_index = + (when_index + 1..end_index).find(|&index| tokens[index].token_type == TokenType::Then)?; + let condition = char_slice( + sql, + tokens[when_index].span.end, + tokens[then_index].span.start, + )? + .trim(); + (!condition.is_empty()).then(|| LintEdit { + start: span.start, + end: span.end, + replacement: format!("COALESCE({condition}, FALSE)"), + }) +} + +fn explicit_union_fix(dialect: DialectType, span: Span) -> Option { + matches!( + dialect, + DialectType::Generic + | DialectType::PostgreSQL + | DialectType::MySQL + | DialectType::BigQuery + | DialectType::Snowflake + | DialectType::DuckDB + | DialectType::Hive + | DialectType::Spark + | DialectType::Trino + | DialectType::Presto + | DialectType::Redshift + | DialectType::ClickHouse + | DialectType::Databricks + | DialectType::Athena + ) + .then(|| LintEdit { + start: span.start, + end: span.end, + replacement: "UNION DISTINCT".to_string(), + }) +} + +fn char_slice(sql: &str, start: usize, end: usize) -> Option<&str> { + if start > end { + return None; + } + let byte_start = char_offset_to_byte_offset(sql, start)?; + let byte_end = char_offset_to_byte_offset(sql, end)?; + sql.get(byte_start..byte_end) +} + +fn char_offset_to_byte_offset(sql: &str, offset: usize) -> Option { + if offset == sql.chars().count() { + return Some(sql.len()); + } + sql.char_indices() + .nth(offset) + .map(|(byte_offset, _)| byte_offset) +} + +fn contains_comment(source: &str) -> bool { + source.contains("--") || source.contains("/*") +} + +fn diagnostics_for_spans( + rule: &LintRuleMetadata, + spans: &[Span], + fix_unavailable_reason: Option<&'static str>, +) -> Vec { + spans + .iter() + .map(|span| diagnostic(rule, Some(*span), None, fix_unavailable_reason)) + .collect() +} + +fn diagnostic( + rule: &LintRuleMetadata, + span: Option, + fix: Option, + fix_unavailable_reason: Option<&'static str>, +) -> LintDiagnostic { + let span = span.unwrap_or_default(); + LintDiagnostic { + code: rule.code, + message: Cow::Borrowed(rule.message), + remediation: Cow::Borrowed(rule.remediation), + start: span.start, + end: span.end, + fix_unavailable_reason: fix.is_none().then_some(fix_unavailable_reason).flatten(), + fix, + } +} + +fn unused_cte_spans(tokens: &[Token], unused_names: &[String]) -> Vec { + let mut remaining: Vec = unused_names + .iter() + .map(|name| name.to_ascii_lowercase()) + .collect(); + let mut spans: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + let name = token.text.to_ascii_lowercase(); + let Some(position) = remaining.iter().position(|candidate| candidate == &name) else { + continue; + }; + if tokens + .get(index + 1) + .is_some_and(|next| next.token_type == TokenType::As) + { + remaining.remove(position); + spans.push(token.span); + } + } + spans +} + +fn null_comparison_spans(tokens: &[Token]) -> Vec { + let comparison_types = [ + TokenType::Eq, + TokenType::Neq, + TokenType::Lt, + TokenType::Lte, + TokenType::Gt, + TokenType::Gte, + ]; + let depths = token_depths(tokens); + tokens + .iter() + .enumerate() + .filter(|(_, token)| comparison_types.contains(&token.token_type)) + .filter(|(index, _)| !is_assignment_operator(tokens, &depths, *index)) + .filter(|(index, _)| { + adjacent_null(tokens, *index, false) || adjacent_null(tokens, *index, true) + }) + .map(|(_, token)| token.span) + .collect() +} + +fn is_assignment_operator(tokens: &[Token], depths: &[usize], operator_index: usize) -> bool { + let depth = depths[operator_index]; + for index in (0..operator_index).rev() { + if depths[index] != depth { + continue; + } + match tokens[index].token_type { + TokenType::Where | TokenType::Having | TokenType::Qualify | TokenType::On => { + return false; + } + TokenType::Set => return true, + TokenType::Select | TokenType::Semicolon => return false, + _ => {} + } + } + false +} + +fn adjacent_null(tokens: &[Token], operator_index: usize, forward: bool) -> bool { + let mut index = operator_index as isize + if forward { 1 } else { -1 }; + let mut wrappers = 0_usize; + loop { + if index < 0 { + return false; + } + let Some(token) = tokens.get(index as usize) else { + return false; + }; + if token.token_type == TokenType::Null { + break; + } + let wrapper = if forward { + TokenType::LParen + } else { + TokenType::RParen + }; + if token.token_type != wrapper { + return false; + } + wrappers += 1; + index += if forward { 1 } else { -1 }; + } + index += if forward { 1 } else { -1 }; + let closing_wrapper = if forward { + TokenType::RParen + } else { + TokenType::LParen + }; + for _ in 0..wrappers { + if index < 0 { + return false; + } + let Some(token) = tokens.get(index as usize) else { + return false; + }; + if token.token_type != closing_wrapper { + return false; + } + index += if forward { 1 } else { -1 }; + } + true +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/formatter.rs b/crates/sqlclean/src/sql_lint/_helpers/formatter.rs new file mode 100644 index 0000000..c162402 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/formatter.rs @@ -0,0 +1,343 @@ +use std::collections::{BTreeMap, HashMap}; +use std::str::FromStr; + +use polyglot_sql::tokens::{Token, TokenType}; +use polyglot_sql::{ComplexityGuardOptions, Dialect, DialectType, ParseOptions}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +use crate::constants::LINT_SENTINEL_PREFIX; +use crate::sql_lint::_helpers::formatter_syntax::{cte_macro_indices, protect_syntax}; +use crate::sql_lint::_helpers::printed_layout::{PrintedSql, WrapOptions, wrap_lines}; +use crate::sql_lint::_helpers::token_layout::{ + AuthoredSql, KeywordRecase, OracleLayout, TokenTexts, comments_in, neutralize_comments, + oracle_layout, print_authored_tokens, tokenize, verify_printed_tokens, +}; +use crate::sql_lint::constants::{ + CAST_TYPE_SEPARATOR_KEYWORD, CLOSE_PARENTHESIS, LINT_API_VERSION, OPEN_PARENTHESIS, +}; +use crate::sql_lint::models::{FormatRequest, FormatResponse}; + +#[cfg(test)] +thread_local! { + /// Idempotency re-prints on this thread, for tests that assert which bodies are re-printed. + pub(crate) static RELAYOUTS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +const NOT_IDEMPOTENT_FAILURE: &str = "native formatter output is not idempotent"; +const UNSUPPORTED_SQL_FAILURE: &str = + "native formatter could not safely restore literal or cast spelling"; + +pub(crate) fn format_json_impl(request_json: &str) -> Result { + let request: FormatRequest = + serde_json::from_str(request_json).map_err(|error| error.to_string())?; + let response = format_sql(request)?; + serde_json::to_string(&response).map_err(|error| error.to_string()) +} + +/// Format requests in parallel, in order; a request that fails is declined with its reason. +pub(crate) fn format_requests(requests: Vec) -> Vec { + requests + .into_par_iter() + .map(|request| { + let original: String = request.sql.clone(); + format_sql(request).unwrap_or_else(|reason| FormatResponse { + version: LINT_API_VERSION, + sql: original, + changed: false, + formatted: false, + reason: Some(reason), + }) + }) + .collect() +} + +fn format_sql(request: FormatRequest) -> Result { + let original = request.sql.clone(); + match try_format_sql(request) { + Ok(value) => Ok(value), + Err(reason) => Ok(FormatResponse { + version: LINT_API_VERSION, + sql: original, + changed: false, + formatted: false, + reason: Some(reason), + }), + } +} + +fn try_format_sql(request: FormatRequest) -> Result { + if request.version != LINT_API_VERSION { + return Err(format!( + "unsupported native format request version {}; expected {LINT_API_VERSION}", + request.version + )); + } + let original = request.sql; + let dialect_type = + DialectType::from_str(&request.dialect).map_err(|error| error.to_string())?; + let dialect = Dialect::get(dialect_type); + let parse_options = ParseOptions { + complexity_guard: Some(ComplexityGuardOptions { + max_function_call_depth: request.max_function_call_depth, + ..Default::default() + }), + }; + let wrap = request.line_width.map(|line_width| WrapOptions { + line_width, + token_widths: &request.token_widths, + }); + let layout = Layout { + dialect: &dialect, + wrap: wrap.as_ref(), + }; + let (formatted, formatted_tokens, oracle) = format_layout(&original, &layout, &parse_options)?; + let changed = formatted != original; + if changed && relayout((&formatted, &formatted_tokens), oracle.as_ref(), &layout)? != formatted + { + return Err(NOT_IDEMPOTENT_FAILURE.to_string()); + } + Ok(FormatResponse { + version: LINT_API_VERSION, + changed, + sql: formatted, + formatted: true, + reason: None, + }) +} + +/// The printed output of [`format_layout`], its tokens, and the oracle it was printed against. +type FormattedLayout = (String, Vec, Option); + +/// Print the tokens of `sql` with parse-tree whitespace; return the output and its oracle. +fn format_layout( + sql: &str, + layout: &Layout<'_>, + parse_options: &ParseOptions, +) -> Result { + let dialect = layout.dialect; + let tokens = dialect + .tokenize(sql) + .map_err(|error| format!("native formatter tokenization failed: {error}"))?; + if tokens.is_empty() { + return Ok((sql.to_string(), tokens, None)); + } + let comments = comments_in(sql, &tokens); + let neutral = neutralize_comments(sql, &comments); + let validation_sql = protect_syntax(&neutral, &tokens, &[], false)?; + dialect + .parse_with_options(&validation_sql.sql, parse_options) + .map_err(|error| format!("native formatter could not parse SQL: {error}"))?; + let semantic_tokens = without_statement_terminators(&tokens); + let oracle = oracle_layout( + &layout_oracle(&neutral, &semantic_tokens, dialect, parse_options)?, + dialect, + )?; + let authored = AuthoredSql { + sql, + tokens: &tokens, + comments: &comments, + }; + let (printed, recases, authored_texts) = layout.print(&authored, &oracle)?; + if printed == sql { + return Ok((printed, tokens, Some(oracle))); + } + let printed_tokens = tokenize(&printed, dialect)?; + verify_printed_tokens( + (sql, &tokens, &authored_texts), + (&printed, &printed_tokens), + dialect, + &recases, + )?; + Ok((printed, printed_tokens, Some(oracle))) +} + +/// Re-print formatted SQL against its oracle; unchanged tokens imply an unchanged layout. +fn relayout( + (formatted, tokens): (&str, &[Token]), + oracle: Option<&OracleLayout>, + layout: &Layout<'_>, +) -> Result { + #[cfg(test)] + RELAYOUTS.with(|relayouts| relayouts.set(relayouts.get() + 1)); + let Some(oracle) = oracle.filter(|_| !tokens.is_empty()) else { + return Ok(formatted.to_string()); + }; + let comments = comments_in(formatted, tokens); + let authored = AuthoredSql { + sql: formatted, + tokens, + comments: &comments, + }; + Ok(layout.print(&authored, oracle)?.0) +} + +/// The dialect and optional line wrapping every printed layout uses. +struct Layout<'a> { + dialect: &'a Dialect, + wrap: Option<&'a WrapOptions<'a>>, +} + +impl Layout<'_> { + fn print( + &self, + authored: &AuthoredSql<'_>, + oracle: &OracleLayout, + ) -> Result<(String, BTreeMap, TokenTexts), String> { + let (printed, recases, texts) = print_authored_tokens(authored, oracle, self.dialect)?; + let no_widths: HashMap = HashMap::new(); + let token_widths = self.wrap.map_or(&no_widths, |wrap| wrap.token_widths); + let Some(mut sql) = PrintedSql::parse(&printed, self.dialect, token_widths)? else { + return Ok((printed, recases, texts)); + }; + sql.arrange_clauses(self.wrap.map_or(usize::MAX, |wrap| wrap.line_width)); + if let Some(wrap) = self.wrap { + wrap_lines(&mut sql, wrap.line_width); + } + sql.lead_operators_of_multiline_operands(); + Ok((sql.render(), recases, texts)) + } +} + +/// Return the parse-tree layout of `neutral_sql`; only its whitespace is ever printed. +fn layout_oracle( + neutral_sql: &str, + semantic_tokens: &[Token], + dialect: &Dialect, + parse_options: &ParseOptions, +) -> Result { + let protected = protect_syntax( + neutral_sql, + semantic_tokens, + &cast_type_token_ranges(neutral_sql, semantic_tokens)?, + true, + )?; + let expressions = dialect + .parse_with_options(&protected.sql, parse_options) + .map_err(|error| error.to_string())?; + let statements: Result, String> = expressions + .iter() + .map(|expression| { + dialect + .generate_pretty(expression) + .map_err(|error| error.to_string()) + }) + .collect(); + let restored = protected.restore(statements?.join(";\n"), dialect)?; + layout_cte_macros(restored, dialect) +} + +fn layout_cte_macros(mut formatted: String, dialect: &Dialect) -> Result { + if !formatted.contains(LINT_SENTINEL_PREFIX) { + return Ok(formatted); + } + let tokens = dialect + .tokenize(&formatted) + .map_err(|error| error.to_string())?; + let macros = cte_macro_indices(&tokens); + if macros.is_empty() { + return Ok(formatted); + } + let depths = crate::sql_lint::_helpers::engine::token_depths(&tokens); + let chars: Vec = formatted.chars().collect(); + let mut gaps: BTreeMap<(usize, usize), String> = BTreeMap::new(); + for index in macros { + let with = (0..index) + .rev() + .find(|&previous| { + tokens[previous].token_type == TokenType::With && depths[previous] == depths[index] + }) + .ok_or_else(|| "native formatter lost a CTE macro scope".to_string())?; + let indentation = line_indentation(&chars, tokens[with].span.start); + let newline = format!("\n{}", " ".repeat(indentation)); + gaps.insert( + (tokens[index - 1].span.end, tokens[index].span.start), + newline.clone(), + ); + if tokens + .get(index + 1) + .is_some_and(|token| token.token_type == TokenType::Comma) + && let Some(next) = tokens.get(index + 2) + { + gaps.insert((tokens[index + 1].span.end, next.span.start), newline); + } + } + for ((start, end), replacement) in gaps.into_iter().rev() { + let start = char_to_byte(&formatted, start)?; + let end = char_to_byte(&formatted, end)?; + formatted.replace_range(start..end, &replacement); + } + Ok(formatted) +} + +fn line_indentation(characters: &[char], offset: usize) -> usize { + let line_start = characters[..offset] + .iter() + .rposition(|character| *character == '\n') + .map_or(0, |position| position + 1); + characters[line_start..offset] + .iter() + .take_while(|character| **character == ' ' || **character == '\t') + .count() +} + +fn cast_type_token_ranges(sql: &str, tokens: &[Token]) -> Result, String> { + let characters: Vec = sql.chars().collect(); + let mut ranges: Vec<(usize, usize)> = Vec::new(); + let mut cast_type_starts: Vec> = Vec::new(); + let mut previous: Option = None; + for (index, token) in tokens.iter().enumerate() { + let text: String = characters + .get(token.span.start..token.span.end) + .ok_or_else(|| UNSUPPORTED_SQL_FAILURE.to_string())? + .iter() + .collect(); + let normalized = text.to_ascii_uppercase(); + if normalized == OPEN_PARENTHESIS { + let opens_cast = previous + .as_deref() + .is_some_and(|value| matches!(value, "CAST" | "TRY_CAST")); + cast_type_starts.push(opens_cast.then_some(usize::MAX)); + } else if normalized == CLOSE_PARENTHESIS { + if let Some(Some(type_start)) = cast_type_starts.pop() + && type_start != usize::MAX + && type_start < index + { + ranges.push((type_start, index - 1)); + } + } else if normalized == CAST_TYPE_SEPARATOR_KEYWORD + && let Some(Some(type_start)) = cast_type_starts.last_mut() + && *type_start == usize::MAX + { + *type_start = index + 1; + } + previous = Some(normalized); + } + ranges.sort_unstable(); + Ok(ranges) +} + +fn without_statement_terminators(tokens: &[Token]) -> Vec { + tokens + .iter() + .enumerate() + .filter(|(index, token)| { + token.token_type != TokenType::Semicolon + && !(token.token_type == TokenType::Comma + && tokens + .get(index + 1) + .is_some_and(|next| next.token_type == TokenType::From)) + }) + .map(|(_, token)| token.clone()) + .collect() +} + +fn char_to_byte(value: &str, char_offset: usize) -> Result { + if char_offset == value.chars().count() { + return Ok(value.len()); + } + value + .char_indices() + .nth(char_offset) + .map(|(offset, _)| offset) + .ok_or_else(|| "native formatter produced an invalid character offset".to_string()) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/formatter_syntax.rs b/crates/sqlclean/src/sql_lint/_helpers/formatter_syntax.rs new file mode 100644 index 0000000..c53a540 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/formatter_syntax.rs @@ -0,0 +1,459 @@ +use crate::constants::LINT_SENTINEL_PREFIX; +use crate::sql_lint::_helpers::engine::token_depths; +use crate::sql_lint::constants::{ + CTE_MACRO_PLACEHOLDER_LITERAL, POSITION_ARGUMENT_SEPARATOR, POSTFIX_CAST_PREFIX_TOKEN_COUNT, +}; +use polyglot_sql::Dialect; +use polyglot_sql::tokens::{Token, TokenType}; +use std::collections::{HashMap, HashSet}; + +pub(super) struct ProtectedSql { + pub sql: String, + /// The lower-case prefix every sentinel name starts with. + prefix: String, + fragments: Vec<(String, String)>, + in_functions: HashSet, +} + +impl ProtectedSql { + pub(super) fn restore(&self, formatted: String, dialect: &Dialect) -> Result { + let tokens = dialect + .tokenize(&formatted) + .map_err(|error| error.to_string())?; + let mut positions: HashMap> = HashMap::new(); + for (index, token) in tokens.iter().enumerate() { + let prefixed = token + .text + .as_bytes() + .get(..self.prefix.len()) + .is_some_and(|start| start.eq_ignore_ascii_case(self.prefix.as_bytes())); + if !prefixed { + continue; + } + positions + .entry(token.text.to_ascii_lowercase()) + .or_default() + .push(index); + } + let mut edits: Vec<(usize, usize, String)> = Vec::new(); + for (placeholder, original) in &self.fragments { + let key = cte_placeholder_sentinel(placeholder) + .unwrap_or(placeholder) + .trim_start_matches(':') + .trim_start_matches('.') + .trim_matches('\'') + .to_ascii_lowercase(); + let found = positions + .get(&key) + .filter(|found| found.len() == 1) + .ok_or_else(|| { + "native formatter could not restore a protected SQL fragment".to_string() + })?; + let index = found[0]; + if self.in_functions.contains(&key) { + let mut depth = 0; + let separator = (index + 1..tokens.len()) + .find(|&position| { + match tokens[position].token_type { + TokenType::LParen => depth += 1, + TokenType::RParen => depth -= 1, + _ => {} + } + depth == 1 && tokens[position].token_type == TokenType::Comma + }) + .ok_or_else(|| "native formatter lost a POSITION separator".to_string())?; + edits.push(( + tokens[separator].span.start, + tokens[separator].span.end, + " IN".to_string(), + )); + } + if let Some(sentinel) = cte_placeholder_sentinel(placeholder) { + let _ = sentinel; + edits.push(restore_cte_macro(&tokens, index, original)?); + continue; + } + if placeholder.starts_with("::") { + edits.extend(restore_postfix_cast(&tokens, index, original)?); + continue; + } + let start = if placeholder.starts_with('.') { + if index == 0 || tokens[index - 1].token_type != TokenType::Dot { + return Err("native formatter changed a protected path accessor".to_string()); + } + tokens[index - 1].span.start + } else { + tokens[index].span.start + }; + edits.push((start, tokens[index].span.end, original.clone())); + } + edits.sort_by_key(|edit| edit.0); + let chars: Vec = formatted.chars().collect(); + let mut result = String::new(); + let mut cursor = 0; + for (start, end, replacement) in edits { + if start < cursor { + return Err("native formatter produced overlapping syntax restorations".to_string()); + } + result.extend(&chars[cursor..start]); + result.push_str(&replacement); + cursor = end; + } + result.extend(&chars[cursor..]); + Ok(result) + } +} + +/// The sentinel of a CTE macro placeholder, with or without the comma added after it. +fn cte_placeholder_sentinel(placeholder: &str) -> Option<&str> { + let placeholder: &str = placeholder.strip_suffix(',').unwrap_or(placeholder); + placeholder.strip_suffix(" AS (SELECT 1)") +} + +pub(super) fn protect_syntax( + sql: &str, + tokens: &[Token], + cast_types: &[(usize, usize)], + expressions: bool, +) -> Result { + let mut prefix = "__sqlbuild_format_".to_string(); + let lowercase: String = sql.to_ascii_lowercase(); + while lowercase.contains(&prefix) { + prefix.push('_'); + } + let mut cast_type_ends: HashMap = HashMap::new(); + for (start, end) in cast_types { + let _ = cast_type_ends.entry(*start).or_insert(*end); + } + let mut ranges: Vec<(usize, usize, String)> = Vec::new(); + let cte_macros = cte_macro_indices(tokens); + let depths = token_depths(tokens); + let mut separators: HashSet = HashSet::new(); + let mut in_functions: HashSet = HashSet::new(); + let mut index = 0; + while index < tokens.len() { + let token = &tokens[index]; + let sentinel = || format!("{prefix}{}__", ranges.len()); + if separators.contains(&index) { + ranges.push((token.span.start, token.span.end, ",".to_string())); + index += 1; + continue; + } + let (end, replacement) = if cte_macros.contains(&index) { + let separator: &str = if cte_macro_needs_comma(tokens, index) { + "," + } else { + "" + }; + (index, format!("{} AS (SELECT 1){separator}", sentinel())) + } else if !expressions { + index += 1; + continue; + } else if let Some(end) = cast_type_ends.get(&index) { + (*end, sentinel()) + } else if token.token_type == TokenType::DColon { + ( + postfix_type_end(tokens, index + 1)?, + format!("::{}", sentinel()), + ) + } else if token.token_type == TokenType::Colon + && index > 0 + && tokens[index - 1].token_type != TokenType::String + && tokens.get(index + 1).is_some_and(|next| { + next.text + .chars() + .next() + .is_some_and(|character| character.is_alphanumeric() || character == '_') + || matches!( + next.token_type, + TokenType::QuotedIdentifier | TokenType::String + ) + }) + { + (index + 1, format!(".{}", sentinel())) + } else if is_typed_literal(tokens, index) + || (matches!(token.token_type, TokenType::Var | TokenType::Identifier) + && tokens.get(index + 1).is_some_and(|next| { + matches!(next.token_type, TokenType::Var | TokenType::Identifier) + }) + && tokens + .get(index + 2) + .is_some_and(|next| next.token_type == TokenType::Arrow)) + { + (index + 1, sentinel()) + } else if (token.text.eq_ignore_ascii_case("MOD") + || token.text.eq_ignore_ascii_case("POSITION")) + && tokens + .get(index + 1) + .is_some_and(|next| next.token_type == TokenType::LParen) + { + let sentinel: String = sentinel(); + if token.text.eq_ignore_ascii_case("POSITION") { + let separator = (index + 2..tokens.len()) + .take_while(|&position| { + !(tokens[position].token_type == TokenType::RParen + && depths[position] == depths[index] + 1) + }) + .find(|&position| { + tokens[position].token_type == TokenType::In + && depths[position] == depths[index] + 1 + }); + if let Some(separator) = separator { + separators.insert(separator); + in_functions.insert(sentinel.clone()); + } + } + (index, sentinel) + } else if matches!( + token.token_type, + TokenType::String | TokenType::DollarString + ) { + (index, format!("'{}'", sentinel())) + } else { + index += 1; + continue; + }; + ranges.push((token.span.start, tokens[end].span.end, replacement)); + index = end + 1; + } + let chars: Vec = sql.chars().collect(); + let token_indices: HashMap = tokens + .iter() + .enumerate() + .map(|(index, token)| (token.span.start, index)) + .collect(); + let mut prepared = String::new(); + let mut fragments: Vec<(String, String)> = Vec::new(); + let mut cursor = 0; + for (start, end, replacement) in ranges { + prepared.extend(&chars[cursor..start]); + prepared.push_str(&replacement); + if replacement != POSITION_ARGUMENT_SEPARATOR { + let first = token_indices[&start]; + let last = (first..tokens.len()) + .find(|&index| tokens[index].span.end == end) + .ok_or_else(|| "native formatter lost a source fragment boundary".to_string())?; + fragments.push(( + replacement, + canonical_fragment(&chars, &tokens[first..=last]), + )); + } + cursor = end; + } + prepared.extend(&chars[cursor..]); + Ok(ProtectedSql { + sql: prepared, + prefix, + fragments, + in_functions, + }) +} + +fn canonical_fragment(chars: &[char], tokens: &[Token]) -> String { + let mut result = String::new(); + for (index, token) in tokens.iter().enumerate() { + if index > 0 + && !matches!( + token.token_type, + TokenType::LParen + | TokenType::RParen + | TokenType::Comma + | TokenType::Dot + | TokenType::Colon + | TokenType::DColon + | TokenType::LBracket + | TokenType::RBracket + | TokenType::Lt + | TokenType::Gt + ) + && !matches!( + tokens[index - 1].token_type, + TokenType::LParen + | TokenType::Dot + | TokenType::Colon + | TokenType::DColon + | TokenType::LBracket + | TokenType::Lt + ) + { + result.push(' '); + } + result.extend(&chars[token.span.start..token.span.end]); + } + result +} + +pub(super) fn cte_macro_indices(tokens: &[Token]) -> HashSet { + let depths = token_depths(tokens); + let mut scopes: HashSet = HashSet::new(); + let mut indices: HashSet = HashSet::new(); + for (index, token) in tokens.iter().enumerate() { + if token.token_type == TokenType::With { + scopes.insert(depths[index]); + } else if token.token_type == TokenType::Select { + scopes.remove(&depths[index]); + } else if token.text.starts_with(LINT_SENTINEL_PREFIX) + && scopes.contains(&depths[index]) + && index > 0 + && (matches!( + tokens[index - 1].token_type, + TokenType::With | TokenType::Comma + ) || indices.contains(&(index - 1))) + && tokens.get(index + 1).is_some_and(|next| { + matches!(next.token_type, TokenType::Comma | TokenType::Select) + || starts_cte(tokens, index + 1) + }) + { + indices.insert(index); + } + } + indices +} + +/// Whether a CTE definition, `name AS (` or another macro sentinel, starts at token `index`. +fn starts_cte(tokens: &[Token], index: usize) -> bool { + let Some(token) = tokens.get(index) else { + return false; + }; + if token.text.starts_with(LINT_SENTINEL_PREFIX) { + return true; + } + let named: bool = matches!( + token.token_type, + TokenType::Var | TokenType::Identifier | TokenType::QuotedIdentifier + ) || token + .text + .chars() + .all(|character| character.is_alphanumeric() || character == '_'); + named + && tokens + .get(index + 1) + .is_some_and(|next| next.token_type == TokenType::As) + && tokens + .get(index + 2) + .is_some_and(|open| open.token_type == TokenType::LParen) +} + +/// Whether the CTE macro sentinel at token `index` needs a comma to separate it from the next CTE. +pub(super) fn cte_macro_needs_comma(tokens: &[Token], index: usize) -> bool { + starts_cte(tokens, index + 1) +} + +fn restore_cte_macro( + tokens: &[Token], + start: usize, + original: &str, +) -> Result<(usize, usize, String), String> { + let end = start + 5; + if tokens + .get(end) + .is_none_or(|token| token.token_type != TokenType::RParen) + || tokens[start + 1].token_type != TokenType::As + || tokens[start + 2].token_type != TokenType::LParen + || tokens[start + 3].token_type != TokenType::Select + || tokens[start + 4].text != CTE_MACRO_PLACEHOLDER_LITERAL + { + return Err("native formatter changed a CTE macro placeholder".to_string()); + } + Ok(( + tokens[start].span.start, + tokens[end].span.end, + original.to_string(), + )) +} + +fn restore_postfix_cast( + tokens: &[Token], + index: usize, + original: &str, +) -> Result, String> { + if index < POSTFIX_CAST_PREFIX_TOKEN_COUNT + || tokens[index - 1].token_type != TokenType::As + || tokens + .get(index + 1) + .is_none_or(|token| token.token_type != TokenType::RParen) + { + return Err("native formatter changed a postfix cast wrapper".to_string()); + } + let mut depth = 0; + let mut open = None; + for position in (0..index - 1).rev() { + match tokens[position].token_type { + TokenType::RParen => depth += 1, + TokenType::LParen if depth == 0 => { + open = Some(position); + break; + } + TokenType::LParen => depth -= 1, + _ => {} + } + } + let open = open + .filter(|&position| position > 0 && tokens[position - 1].token_type == TokenType::Cast) + .ok_or_else(|| "native formatter could not identify a postfix cast wrapper".to_string())?; + Ok(vec![ + ( + tokens[open - 1].span.start, + tokens[open + 1].span.start, + String::new(), + ), + ( + tokens[index - 2].span.end, + tokens[index + 1].span.end, + original.to_string(), + ), + ]) +} + +fn is_typed_literal(tokens: &[Token], index: usize) -> bool { + matches!( + tokens[index].text.to_ascii_uppercase().as_str(), + "DATE" + | "TIME" + | "TIMESTAMP" + | "TIMESTAMPTZ" + | "TIMESTAMP_NTZ" + | "TIMESTAMP_LTZ" + | "TIMESTAMP_TZ" + ) && tokens.get(index + 1).is_some_and(|token| { + matches!( + token.token_type, + TokenType::String | TokenType::DollarString + ) + }) +} + +fn postfix_type_end(tokens: &[Token], start: usize) -> Result { + let Some(_) = tokens.get(start) else { + return Err("native formatter could not locate a postfix cast type".to_string()); + }; + let mut end = start; + while tokens.get(end + 1).is_some_and(|token| { + matches!( + token.text.to_ascii_uppercase().as_str(), + "PRECISION" | "VARYING" | "WITH" | "WITHOUT" | "TIME" | "ZONE" + ) + }) { + end += 1; + } + if tokens + .get(end + 1) + .is_some_and(|token| token.token_type == TokenType::LParen) + { + let mut depth = 0; + for (index, token) in tokens.iter().enumerate().skip(end + 1) { + match token.token_type { + TokenType::LParen => depth += 1, + TokenType::RParen => { + depth -= 1; + if depth == 0 { + return Ok(index); + } + } + _ => {} + } + } + return Err("native formatter could not locate postfix cast parameters".to_string()); + } + Ok(end) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/inline_relations.rs b/crates/sqlclean/src/sql_lint/_helpers/inline_relations.rs new file mode 100644 index 0000000..7660a13 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/inline_relations.rs @@ -0,0 +1,223 @@ +use polyglot_sql::tokens::{Span, Token, TokenType}; + +use crate::sql_lint::_helpers::engine::is_query_from; + +struct InlineRelationContext<'a> { + tokens: &'a [Token], + depths: &'a [usize], + scopes: Vec>, + significant: &'a [usize], +} + +pub(super) fn collect_inline_query_relation_spans( + tokens: &[Token], + depths: &[usize], + significant: &[usize], +) -> Vec { + let context = InlineRelationContext { + tokens, + depths, + scopes: token_scopes(tokens), + significant, + }; + (0..significant.len()) + .filter_map(|position| inline_query_relation_span(&context, position)) + .collect() +} + +fn inline_query_relation_span( + context: &InlineRelationContext<'_>, + position: usize, +) -> Option { + let &opening = context.significant.get(position)?; + if context.tokens[opening].token_type != TokenType::LParen { + return None; + } + let marker_position = position.checked_sub(1)?; + let previous = context.significant[marker_position]; + if context.tokens[previous].token_type == TokenType::Lateral + || context.tokens[previous].text.eq_ignore_ascii_case("apply") + || !is_query_relation_marker(context, marker_position) + { + return None; + } + + let mut candidate_position = position + 1; + let mut candidate_depth = context.depths[opening] + 1; + loop { + let &candidate = context.significant.get(candidate_position)?; + if context.depths[candidate] != candidate_depth { + return None; + } + if context.tokens[candidate].token_type == TokenType::LParen { + candidate_position += 1; + candidate_depth += 1; + continue; + } + return is_query_start(&context.tokens[candidate]) + .then_some(context.tokens[candidate].span); + } +} + +fn is_query_relation_marker(context: &InlineRelationContext<'_>, marker_position: usize) -> bool { + is_query_relation_marker_base(context, marker_position) + && !query_or_ancestor_is_ignored(context, marker_position) +} + +fn is_query_relation_marker_base( + context: &InlineRelationContext<'_>, + marker_position: usize, +) -> bool { + let marker = context.significant[marker_position]; + if !(is_query_from(context.tokens, marker) + || matches!( + context.tokens[marker].token_type, + TokenType::Join | TokenType::Comma + )) + { + return false; + } + let scope = context.scopes[marker]; + let Some(select_position) = (0..marker_position).rev().find(|&position| { + let index = context.significant[position]; + context.scopes[index] == scope && context.tokens[index].token_type == TokenType::Select + }) else { + return false; + }; + let query_clause = &context.significant[select_position + 1..marker_position]; + if query_clause.iter().any(|&index| { + context.scopes[index] == scope + && matches!( + context.tokens[index].token_type, + TokenType::Semicolon | TokenType::Union | TokenType::Intersect | TokenType::Except + ) + }) { + return false; + } + if is_query_from(context.tokens, marker) { + return true; + } + let Some(from_position) = query_clause + .iter() + .position(|&index| context.scopes[index] == scope && is_query_from(context.tokens, index)) + else { + return false; + }; + !query_clause[from_position + 1..].iter().any(|&index| { + context.scopes[index] == scope && is_post_relation_clause(context.tokens[index].token_type) + }) +} + +fn query_or_ancestor_is_ignored( + context: &InlineRelationContext<'_>, + marker_position: usize, +) -> bool { + let marker = context.significant[marker_position]; + let mut scope = context.scopes[marker]; + while let Some(opening) = scope { + if scope_starts_query(context, opening) && query_scope_is_ignored(context, opening) { + return true; + } + scope = context.scopes[opening]; + } + false +} + +fn scope_starts_query(context: &InlineRelationContext<'_>, opening: usize) -> bool { + let Ok(mut position) = context.significant.binary_search(&opening) else { + return false; + }; + position += 1; + let mut expected_depth = context.depths[opening] + 1; + loop { + let Some(&candidate) = context.significant.get(position) else { + return false; + }; + if context.depths[candidate] != expected_depth { + return false; + } + if context.tokens[candidate].token_type == TokenType::LParen { + position += 1; + expected_depth += 1; + continue; + } + return is_query_start(&context.tokens[candidate]); + } +} + +fn query_scope_is_ignored(context: &InlineRelationContext<'_>, opening: usize) -> bool { + let Ok(mut opening_position) = context.significant.binary_search(&opening) else { + return false; + }; + while opening_position > 0 + && context.tokens[context.significant[opening_position - 1]].token_type == TokenType::LParen + { + opening_position -= 1; + } + let Some(marker_position) = opening_position.checked_sub(1) else { + return false; + }; + let marker = context.significant[marker_position]; + match context.tokens[marker].token_type { + TokenType::Exists | TokenType::In => true, + TokenType::From | TokenType::Join | TokenType::As => false, + TokenType::Comma => !is_query_relation_marker_base(context, marker_position), + TokenType::Union + | TokenType::Intersect + | TokenType::Except + | TokenType::All + | TokenType::Distinct + | TokenType::Semicolon + | TokenType::Lateral => false, + _ if context.tokens[marker].text.eq_ignore_ascii_case("apply") + || context.tokens[marker] + .text + .eq_ignore_ascii_case("materialized") => + { + false + } + _ => true, + } +} + +fn is_query_start(token: &Token) -> bool { + matches!(token.token_type, TokenType::Select | TokenType::Values) + || token.text.eq_ignore_ascii_case("with") +} + +fn is_post_relation_clause(token_type: TokenType) -> bool { + matches!( + token_type, + TokenType::Where + | TokenType::Group + | TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + | TokenType::Fetch + ) +} + +fn token_scopes(tokens: &[Token]) -> Vec> { + let mut openings: Vec = Vec::new(); + tokens + .iter() + .enumerate() + .map(|(index, token)| { + let scope = openings.last().copied(); + if matches!( + token.token_type, + TokenType::LParen | TokenType::LBracket | TokenType::LBrace + ) { + openings.push(index); + } else if matches!( + token.token_type, + TokenType::RParen | TokenType::RBracket | TokenType::RBrace + ) { + let _ = openings.pop(); + } + scope + }) + .collect() +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/join_predicates.rs b/crates/sqlclean/src/sql_lint/_helpers/join_predicates.rs new file mode 100644 index 0000000..2b5453d --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/join_predicates.rs @@ -0,0 +1,694 @@ +//! Join predicates, and whether an explicit cross join can multiply rows. + +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + +use polyglot_sql::expressions::{Expression, JoinKind, Literal, Select, Subquery, TableRef, With}; +use polyglot_sql::parser::{Parser, ParserConfig}; +use polyglot_sql::tokens::{Span, Token, TokenType}; +use polyglot_sql::{DialectType, ExpressionWalk}; + +use crate::constants::SETTING_SNIPPET_INDENT; +use crate::sql_lint::constants::{ + CROSS_JOIN_CTE_MARKER, CROSS_JOIN_PATH_PLACEHOLDER, CROSS_JOIN_PREFIX, + CROSS_JOIN_REASON_PLACEHOLDER, CROSS_JOIN_SUFFIX, CROSS_JOIN_UNNAMED_MESSAGE, +}; +use crate::sql_lint::models::{CrossJoinRequest, LintDiagnostic, LintRuleMetadata}; + +const CROSS_JOIN_FIX_REASON: &str = "a keyed relationship requires user intent"; +const FIRST_ROW: &str = "1"; +const ROW_GENERATOR_FUNCTIONS: [&str; 16] = [ + "unnest", + "explode", + "explode_outer", + "posexplode", + "posexplode_outer", + "inline", + "inline_outer", + "flatten", + "generate_series", + "generate_subscripts", + "json_each", + "json_tree", + "json_array_elements", + "jsonb_array_elements", + "range", + "stack", +]; + +pub(super) fn join_expression_spans( + tokens: &[Token], + depths: &[usize], + dialect: DialectType, +) -> Vec { + let mut spans: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if token.token_type != TokenType::On { + continue; + } + let clause = (0..index).rev().find(|&previous| { + depths[previous] == depths[index] + && matches!( + tokens[previous].token_type, + TokenType::Join + | TokenType::Select + | TokenType::Where + | TokenType::From + | TokenType::On + ) + }); + if clause.is_none_or(|previous| tokens[previous].token_type != TokenType::Join) { + continue; + } + let end = (index + 1..tokens.len()) + .find(|&next| { + depths[next] < depths[index] + || (depths[next] == depths[index] + && matches!( + tokens[next].token_type, + TokenType::Join + | TokenType::Where + | TokenType::Group + | TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Union + | TokenType::Intersect + | TokenType::Except + | TokenType::Semicolon + )) + }) + .unwrap_or(tokens.len()); + let mut parser = Parser::with_config( + tokens[index + 1..end].to_vec(), + ParserConfig { + dialect: Some(dialect), + ..Default::default() + }, + ); + if !matches!(parser.parse_expressions(), Ok(Some(ref predicate)) if plain_predicate(predicate)) + { + spans.push(token.span); + } + } + spans +} + +fn plain_column(expression: &Expression) -> bool { + matches!( + expression, + Expression::Column(_) | Expression::Identifier(_) + ) || matches!(expression, Expression::Paren(value) if plain_column(&value.this)) +} + +fn literal(expression: &Expression) -> bool { + matches!( + expression, + Expression::Literal(_) | Expression::Boolean(_) | Expression::Null(_) + ) || matches!(expression, Expression::Neg(value) if matches!(&value.this, Expression::Literal(_))) +} + +fn operand(expression: &Expression) -> bool { + plain_column(expression) || literal(expression) +} + +fn comparison(left: &Expression, right: &Expression) -> bool { + operand(left) && operand(right) +} + +fn plain_predicate(expression: &Expression) -> bool { + match expression { + Expression::Boolean(_) => true, + Expression::Paren(value) => plain_predicate(&value.this), + Expression::And(value) | Expression::Or(value) => { + plain_predicate(&value.left) && plain_predicate(&value.right) + } + Expression::Eq(value) + | Expression::Neq(value) + | Expression::Lt(value) + | Expression::Lte(value) + | Expression::Gt(value) + | Expression::Gte(value) + | Expression::NullSafeEq(value) + | Expression::NullSafeNeq(value) + | Expression::Adjacent(value) + | Expression::ArrayContainsAll(value) + | Expression::ArrayContainedBy(value) + | Expression::ArrayOverlaps(value) + | Expression::ExtendsLeft(value) + | Expression::ExtendsRight(value) + | Expression::Match(value) + | Expression::TsMatch(value) => comparison(&value.left, &value.right), + Expression::Like(value) | Expression::ILike(value) => { + comparison(&value.left, &value.right) + && value.escape.as_ref().is_none_or(literal) + && value.quantifier.is_none() + } + Expression::Between(value) => { + plain_column(&value.this) && operand(&value.low) && operand(&value.high) + } + Expression::In(value) => { + plain_column(&value.this) + && value.query.is_none() + && value.unnest.is_none() + && !value.is_field + && !value.expressions.is_empty() + && value.expressions.iter().all(literal) + } + Expression::IsNull(value) => plain_column(&value.this), + _ => false, + } +} + +/// A `rule` diagnostic for every cross join that can multiply rows, naming its relation and CTE. +pub(super) fn cross_join_diagnostics( + request: &CrossJoinRequest<'_>, + rule: &LintRuleMetadata, +) -> Vec { + let one_row_relations: HashSet = request + .relation_keys + .iter() + .filter(|(_, keys)| keys.iter().any(Vec::is_empty)) + .map(|(name, _)| name.to_ascii_lowercase()) + .collect(); + let relation_names: HashMap = request + .relation_names + .iter() + .map(|(sentinel, name)| (sentinel.to_ascii_lowercase(), name.clone())) + .collect(); + let inputs = OneRowInputs { + one_row_relations: &one_row_relations, + relation_names: &relation_names, + }; + multiplying_cross_joins( + request.statements, + request.tokens, + request.cross_spans, + &inputs, + ) + .into_iter() + .map(|finding| LintDiagnostic { + code: rule.code, + message: finding + .relation + .as_deref() + .map_or(Cow::Borrowed(CROSS_JOIN_UNNAMED_MESSAGE), |relation| { + Cow::Owned(cross_join_message(relation, finding.cte.as_deref())) + }), + remediation: if finding.relation.is_some() { + Cow::Borrowed(rule.remediation) + } else { + Cow::Owned(file_wide_remediation(rule.code)) + }, + start: finding.span.start, + end: finding.span.end, + fix_unavailable_reason: Some(CROSS_JOIN_FIX_REASON), + fix: None, + }) + .collect() +} + +/// Help for a join no relation names: fix it, or record the file-wide exception it allows. +fn file_wide_remediation(code: &str) -> String { + let entry: String = [ + "[[rules.rule_exceptions]]".to_owned(), + format!("rule = \"{code}\""), + format!("path = \"{CROSS_JOIN_PATH_PLACEHOLDER}\""), + format!("reason = \"{CROSS_JOIN_REASON_PLACEHOLDER}\""), + ] + .iter() + .map(|line| format!("\n{SETTING_SNIPPET_INDENT}{line}")) + .collect(); + format!( + "Use a keyed JOIN, or make one side provably at most one row. This join reads no named \ + relation, so a relation-level exception cannot cover it; when the cartesian product is \ + intentional, record a reviewed file-wide exception in the configuration file:{entry}" + ) +} + +/// The finding message naming the joined relation and, inside a CTE, that CTE. +fn cross_join_message(relation: &str, cte: Option<&str>) -> String { + match cte { + Some(cte) => { + format!("{CROSS_JOIN_PREFIX}{relation}{CROSS_JOIN_CTE_MARKER}{cte}{CROSS_JOIN_SUFFIX}") + } + None => format!("{CROSS_JOIN_PREFIX}{relation}{CROSS_JOIN_SUFFIX}"), + } +} + +/// One explicit cross join found in the query tree. +struct CrossJoinTarget { + /// Character offsets where the joined relation may start in the token stream. + anchors: Vec, + /// Whether both joined sides may hold more than one row. + multiplies: bool, + /// The joined relation's name as authored, with dependency sentinels resolved. + relation: Option, + /// The top-level CTE whose query contains the join. + cte: Option, +} + +/// The cross join reported at one `CROSS` keyword. +struct CrossJoinFinding { + span: Span, + relation: Option, + cte: Option, +} + +/// What the analysis knows about relations named in the query. +struct OneRowInputs<'a> { + /// Lowercased dependency sentinels whose relation holds at most one row. + one_row_relations: &'a HashSet, + /// Lowercased dependency sentinels mapped to the relation names they stand for. + relation_names: &'a HashMap, +} + +struct Analysis<'a> { + inputs: &'a OneRowInputs<'a>, + ctes: HashMap, + targets: Vec, +} + +/// Cross joins at `cross_spans` that can multiply rows, each with its relation and CTE. +fn multiplying_cross_joins( + statements: &[Expression], + tokens: &[Token], + cross_spans: &[Span], + inputs: &OneRowInputs<'_>, +) -> Vec { + if cross_spans.is_empty() { + return Vec::new(); + } + let mut analysis = Analysis { + inputs, + ctes: HashMap::new(), + targets: Vec::new(), + }; + for statement in statements { + analysis.record_ctes(statement); + } + for statement in statements { + analysis.visit(statement, None, true); + } + let significant: Vec = (0..tokens.len()) + .filter(|&index| { + !matches!( + tokens[index].token_type, + TokenType::Space + | TokenType::Break + | TokenType::LineComment + | TokenType::BlockComment + ) + }) + .collect(); + let mut findings: Vec = Vec::new(); + for span in cross_spans { + let candidates: Vec = relation_anchor_candidates(tokens, &significant, *span); + let target: Option<&CrossJoinTarget> = analysis + .targets + .iter() + .find(|target| anchored_at(target, &candidates)); + match target { + Some(target) if !target.multiplies => {} + Some(target) => findings.push(CrossJoinFinding { + span: *span, + relation: target.relation.clone(), + cte: target.cte.clone(), + }), + None => findings.push(CrossJoinFinding { + span: *span, + relation: None, + cte: None, + }), + } + } + findings +} + +impl Analysis<'_> { + /// Record whether every CTE holds at most one row, in definition order. + fn record_ctes(&mut self, expression: &Expression) { + for node in expression.dfs() { + let Some(with) = with_clause(node) else { + continue; + }; + for cte in &with.ctes { + let one_row: bool = !with.recursive && self.at_most_one_row(&cte.this); + let name: String = cte.alias.name.to_ascii_lowercase(); + let known: bool = self.ctes.get(&name).copied().unwrap_or(one_row); + self.ctes.insert(name, known && one_row); + } + } + } + + /// Record joins under `expression`, labelled with the top-level CTE containing them. + fn visit(&mut self, expression: &Expression, cte: Option<&str>, top: bool) { + let mut bodies: Vec<&Expression> = Vec::new(); + if let Some(with) = with_clause(expression) { + for definition in &with.ctes { + bodies.push(&definition.this); + let label: Option<&str> = if top { + Some(&definition.alias.name) + } else { + cte + }; + self.visit(&definition.this, label, false); + } + } + if let Expression::Select(select) = expression { + self.record_joins(select, cte); + } + let branches_are_top: bool = top && is_set_operation(expression); + for child in expression.children() { + if bodies.iter().any(|body| std::ptr::eq(*body, child)) { + continue; + } + self.visit(child, cte, branches_are_top); + } + } + + fn record_joins(&mut self, select: &Select, cte: Option<&str>) { + let single_from: Option<&Expression> = select + .from + .as_ref() + .filter(|from| from.expressions.len() == 1) + .and_then(|from| from.expressions.first()); + for (index, join) in select.joins.iter().enumerate() { + if join.kind != JoinKind::Cross { + continue; + } + let left_one_row: bool = + index == 0 && single_from.is_some_and(|from| self.relation_one_row(from)); + let multiplies: bool = !(left_one_row || self.relation_one_row(&join.this)); + self.targets.push(CrossJoinTarget { + anchors: relation_anchors(&join.this), + multiplies, + relation: self.relation_name(&join.this), + cte: cte.map(str::to_owned), + }); + } + } + + fn relation_name(&self, relation: &Expression) -> Option { + match relation { + Expression::Table(table) => Some(self.table_name(table)), + Expression::Subquery(subquery) => { + subquery.alias.as_ref().map(|alias| alias.name.clone()) + } + Expression::Alias(alias) => Some(alias.alias.name.clone()), + _ => None, + } + } + + fn table_name(&self, table: &TableRef) -> String { + if table.schema.is_none() && table.catalog.is_none() { + let lowered: String = table.name.name.to_ascii_lowercase(); + if let Some(name) = self.inputs.relation_names.get(&lowered) { + return name.clone(); + } + } + [&table.catalog, &table.schema] + .into_iter() + .flatten() + .map(|part| part.name.as_str()) + .chain([table.name.name.as_str()]) + .collect::>() + .join(".") + } + + /// Whether a query expression returns at most one row. + fn at_most_one_row(&self, expression: &Expression) -> bool { + match expression { + Expression::Select(select) => self.select_one_row(select), + Expression::Subquery(subquery) => self.subquery_one_row(subquery), + Expression::Paren(paren) => self.at_most_one_row(&paren.this), + Expression::Values(values) => values.expressions.len() <= 1, + _ => false, + } + } + + fn subquery_one_row(&self, subquery: &Subquery) -> bool { + !subquery.lateral + && (subquery + .limit + .as_ref() + .is_some_and(|limit| !limit.percent && at_most_one(&limit.this)) + || self.at_most_one_row(&subquery.this)) + } + + fn select_one_row(&self, select: &Select) -> bool { + if select.limit_by.is_some() || select.connect.is_some() || !select.lateral_views.is_empty() + { + return false; + } + if limited_to_one_row(select) { + return true; + } + if select.expressions.iter().any(generates_rows) { + return false; + } + if select.group_by.is_none() && select.expressions.iter().any(aggregates) { + return true; + } + if select + .qualify + .as_ref() + .is_some_and(|qualify| first_unpartitioned_row(&qualify.this, select)) + { + return true; + } + if select.group_by.is_some() || !select.joins.is_empty() { + return false; + } + match &select.from { + None => true, + Some(from) => match from.expressions.as_slice() { + [relation] => self.relation_one_row(relation), + _ => false, + }, + } + } + + /// Whether a relation in FROM or JOIN holds at most one row. + fn relation_one_row(&self, relation: &Expression) -> bool { + match relation { + Expression::Table(table) => { + if table.schema.is_some() + || table.catalog.is_some() + || table.identifier_func.is_some() + || table.changes.is_some() + { + return false; + } + let name: String = table.name.name.to_ascii_lowercase(); + match self.ctes.get(&name) { + Some(one_row) => *one_row, + None => self.inputs.one_row_relations.contains(&name), + } + } + Expression::Subquery(subquery) => self.subquery_one_row(subquery), + Expression::Paren(paren) => self.relation_one_row(&paren.this), + Expression::Values(values) => values.expressions.len() <= 1, + _ => false, + } + } +} + +/// The WITH clause a query expression owns. +fn with_clause(expression: &Expression) -> Option<&With> { + match expression { + Expression::Select(select) => select.with.as_ref(), + Expression::Union(union) => union.with.as_ref(), + Expression::Intersect(intersect) => intersect.with.as_ref(), + Expression::Except(except) => except.with.as_ref(), + _ => None, + } +} + +fn is_set_operation(expression: &Expression) -> bool { + matches!( + expression, + Expression::Union(_) | Expression::Intersect(_) | Expression::Except(_) + ) +} + +fn limited_to_one_row(select: &Select) -> bool { + let limited: bool = select + .limit + .as_ref() + .is_some_and(|limit| !limit.percent && at_most_one(&limit.this)); + let fetched: bool = select.fetch.as_ref().is_some_and(|fetch| { + !fetch.percent && !fetch.with_ties && fetch.count.as_ref().is_some_and(at_most_one) + }); + let top: bool = select + .top + .as_ref() + .is_some_and(|top| !top.percent && !top.with_ties && at_most_one(&top.this)); + limited || fetched || top +} + +fn at_most_one(expression: &Expression) -> bool { + match expression { + Expression::Literal(literal) => match literal.as_ref() { + Literal::Number(text) => text.parse::().is_ok_and(|count| count <= 1), + _ => false, + }, + Expression::Paren(paren) => at_most_one(&paren.this), + _ => false, + } +} + +/// Whether a projected expression can return several rows per input row. +fn generates_rows(expression: &Expression) -> bool { + expression.dfs().any(|node| match node { + Expression::Unnest(_) + | Expression::Explode(_) + | Expression::ExplodeOuter(_) + | Expression::GenerateSeries(_) => true, + Expression::Function(function) => is_row_generator(&function.name), + _ => false, + }) +} + +fn is_row_generator(name: &str) -> bool { + let name: &str = name.rsplit('.').next().unwrap_or_default(); + ROW_GENERATOR_FUNCTIONS + .iter() + .any(|generator| generator.eq_ignore_ascii_case(name)) +} + +fn anchored_at(target: &CrossJoinTarget, candidates: &[usize]) -> bool { + target + .anchors + .iter() + .any(|anchor| candidates.contains(anchor)) +} + +/// Whether an expression aggregates outside any window function or subquery. +fn aggregates(expression: &Expression) -> bool { + if polyglot_sql::traversal::is_aggregate(expression) { + return true; + } + match expression { + Expression::WindowFunction(_) | Expression::Subquery(_) | Expression::Select(_) => false, + _ => expression.children().into_iter().any(aggregates), + } +} + +/// `QUALIFY ROW_NUMBER() OVER (ORDER BY ...) = 1` without a partition, directly or by alias. +fn first_unpartitioned_row(condition: &Expression, select: &Select) -> bool { + match condition { + Expression::And(operation) => { + first_unpartitioned_row(&operation.left, select) + || first_unpartitioned_row(&operation.right, select) + } + Expression::Paren(paren) => first_unpartitioned_row(&paren.this, select), + Expression::Eq(operation) => { + let compared: &Expression = match (&operation.left, &operation.right) { + (other, Expression::Literal(literal)) | (Expression::Literal(literal), other) + if matches!(literal.as_ref(), Literal::Number(text) if text == FIRST_ROW) => + { + other + } + _ => return false, + }; + match compared { + Expression::WindowFunction(_) => unpartitioned_row_number(compared), + Expression::Column(column) if column.table.is_none() => select + .expressions + .iter() + .any(|projection| match projection { + Expression::Alias(alias) => { + alias.alias.name.eq_ignore_ascii_case(&column.name.name) + && unpartitioned_row_number(&alias.this) + } + _ => false, + }), + _ => false, + } + } + _ => false, + } +} + +fn unpartitioned_row_number(expression: &Expression) -> bool { + matches!( + expression, + Expression::WindowFunction(window) + if matches!(window.this, Expression::RowNumber(_)) + && window.over.window_name.is_none() + && window.over.partition_by.is_empty() + ) +} + +/// Where a joined relation can start: its first name part, or a derived table's alias. +fn relation_anchors(relation: &Expression) -> Vec { + match relation { + Expression::Table(table) => { + let mut anchors: Vec = Vec::new(); + for identifier in [&table.catalog, &table.schema].into_iter().flatten() { + anchors.extend(identifier.span.map(|span| span.start)); + } + anchors.extend(table.name.span.map(|span| span.start)); + anchors.extend(table.span.map(|span| span.start)); + anchors + } + Expression::Subquery(subquery) => subquery + .alias + .as_ref() + .and_then(|alias| alias.span) + .map(|span| span.start) + .into_iter() + .collect(), + Expression::Alias(alias) => alias + .alias + .span + .map(|span| span.start) + .into_iter() + .collect(), + _ => Vec::new(), + } +} + +/// Where the relation after `CROSS JOIN` at `cross` starts, and a derived table's alias. +fn relation_anchor_candidates(tokens: &[Token], significant: &[usize], cross: Span) -> Vec { + let Some(position) = significant + .iter() + .position(|&index| tokens[index].span.start == cross.start) + else { + return Vec::new(); + }; + let Some(&first) = significant.get(position + 2) else { + return Vec::new(); + }; + let mut candidates: Vec = vec![tokens[first].span.start]; + if tokens[first].token_type == TokenType::LParen { + let mut depth: usize = 0; + let mut cursor: usize = position + 2; + while let Some(&index) = significant.get(cursor) { + match tokens[index].token_type { + TokenType::LParen => depth += 1, + TokenType::RParen => { + depth = depth.saturating_sub(1); + if depth == 0 { + break; + } + } + _ => {} + } + cursor += 1; + } + let mut alias: usize = cursor + 1; + if significant + .get(alias) + .is_some_and(|&index| tokens[index].token_type == TokenType::As) + { + alias += 1; + } + if let Some(&index) = significant.get(alias) { + candidates.push(tokens[index].span.start); + } + } + candidates +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/mod.rs b/crates/sqlclean/src/sql_lint/_helpers/mod.rs new file mode 100644 index 0000000..4e13c95 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/mod.rs @@ -0,0 +1,10 @@ +pub mod additional; +pub mod engine; +pub mod formatter; +pub mod formatter_syntax; +pub mod inline_relations; +pub mod join_predicates; +pub mod parsing; +pub mod printed_layout; +pub mod terminal_shape; +pub mod token_layout; diff --git a/crates/sqlclean/src/sql_lint/_helpers/parsing.rs b/crates/sqlclean/src/sql_lint/_helpers/parsing.rs new file mode 100644 index 0000000..d9611a1 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/parsing.rs @@ -0,0 +1,44 @@ +//! The one tokenize-and-parse step every lint and fact request shares. + +use std::str::FromStr; + +use polyglot_sql::parser::ParserConfig; + +use crate::sql_lint::models::ParsedSql; +use polyglot_sql::{ + ComplexityGuardOptions, Dialect, DialectType, Expression, ExpressionWalk, Parser, +}; + +const MAX_LINT_FUNCTION_CALL_DEPTH: usize = 128; + +/// Tokenize and parse `sql`, rejecting statements the parser kept as opaque text. +pub(crate) fn parse(sql: &str, dialect_name: &str) -> Result { + let dialect_type = DialectType::from_str(dialect_name).map_err(|error| error.to_string())?; + let dialect = Dialect::get(dialect_type); + let tokens = dialect.tokenize(sql).map_err(|error| error.to_string())?; + let mut parser = Parser::with_config( + tokens.clone(), + ParserConfig { + dialect: Some(dialect_type), + complexity_guard: ComplexityGuardOptions { + max_function_call_depth: Some(MAX_LINT_FUNCTION_CALL_DEPTH), + ..Default::default() + }, + ..Default::default() + }, + ); + let statements = parser.parse().map_err(|error| error.to_string())?; + for statement in &statements { + if statement + .dfs() + .any(|node| matches!(node, Expression::Command(_) | Expression::Raw(_))) + { + return Err("unsupported syntax: native parser retained an opaque SQL node".to_owned()); + } + } + Ok(ParsedSql { + dialect: dialect_type, + tokens, + statements, + }) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs b/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs new file mode 100644 index 0000000..5aa8522 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs @@ -0,0 +1,931 @@ +//! Printed SQL as tokens and gaps, with clause layout rules and line-width wrapping. + +use std::collections::HashMap; + +use polyglot_sql::Dialect; +use polyglot_sql::tokens::TokenType; + +use crate::sql_lint::constants::OPEN_PARENTHESIS; + +const CONTINUATION_INDENT: usize = 2; +const COMMA: &str = ","; +const SINGLE_SPACE: &str = " "; +const BETWEEN_KEYWORD: &str = "BETWEEN"; +const BETWEEN_SEPARATOR: &str = "AND"; +const VALUES_KEYWORD: &str = "VALUES"; +const DECODE_FUNCTION: &str = "DECODE"; +const CASE_KEYWORD: &str = "CASE"; +const SENTINEL_PREFIX: &str = "__SQB_LINT_"; +const SINGLE_ITEM_CLAUSES: [&str; 4] = ["SELECT", "WHERE", "QUALIFY", "HAVING"]; +const SELECT_MODIFIERS: [&str; 10] = [ + "DISTINCT", "ALL", "TOP", "PERCENT", "WITH", "TIES", "ON", "AS", "STRUCT", "VALUE", +]; +const LEADING_OPERATORS: [&str; 8] = ["+", "-", "*", "/", "%", "||", "AND", "OR"]; +const CHAIN_KEYWORDS: [&str; 2] = ["AND", "OR"]; +const BETWEEN_BOUNDARIES: [&str; 12] = [ + "AND", "OR", "WHEN", "THEN", "ELSE", "WHERE", "ON", "HAVING", "QUALIFY", "SELECT", ",", "NOT", +]; +const ARITHMETIC_OPERATORS: [&str; 5] = ["+", "-", "||", "/", "%"]; +const WINDOW_CLAUSE_KEYWORDS: [&str; 4] = ["ORDER", "ROWS", "RANGE", "GROUPS"]; +const CASE_BRANCH_KEYWORDS: [&str; 3] = ["WHEN", "THEN", "ELSE"]; +const TYPE_PARAMETER_WORDS: [&str; 1] = ["MAX"]; +const TYPE_NAMES: [&str; 30] = [ + "BIGNUMERIC", + "BINARY", + "BIT", + "CHAR", + "CHARACTER", + "DATETIME", + "DATETIME2", + "DATETIMEOFFSET", + "DEC", + "DECIMAL", + "DOUBLE", + "FLOAT", + "INTERVAL", + "NCHAR", + "NUMBER", + "NUMERIC", + "NVARCHAR", + "STRING", + "TEXT", + "TIME", + "TIMESTAMP", + "TIMESTAMPTZ", + "TIMESTAMP_LTZ", + "TIMESTAMP_NTZ", + "TIMESTAMP_TZ", + "TIMETZ", + "VARBINARY", + "VARCHAR", + "BYTES", + "BPCHAR", +]; + +/// Printed SQL whose layout passes only ever rewrite the gaps between tokens. +pub(crate) struct PrintedSql<'a> { + tokens: Vec, + kinds: Vec, + /// The text before each token, plus the text after the last token. + gaps: Vec, + depths: Vec, + partners: Vec>, + /// Gaps that line wrapping must keep: inside VALUES rows and type parameter lists. + locked: Vec, + token_widths: &'a HashMap, +} + +/// One printed line; end columns are absent when comments or multi-line tokens pin it. +struct Line { + first: usize, + last: usize, + indentation: usize, + end_columns: Option>, +} + +impl<'a> PrintedSql<'a> { + pub(crate) fn parse( + sql: &str, + dialect: &Dialect, + token_widths: &'a HashMap, + ) -> Result, String> { + let tokens = dialect + .tokenize(sql) + .map_err(|error| format!("native formatter tokenization failed: {error}"))?; + if tokens.is_empty() { + return Ok(None); + } + let characters: Vec = sql.chars().collect(); + let mut texts: Vec = Vec::with_capacity(tokens.len()); + let mut gaps: Vec = Vec::with_capacity(tokens.len() + 1); + let mut previous_end = 0_usize; + for token in &tokens { + gaps.push(characters[previous_end..token.span.start].iter().collect()); + texts.push( + characters[token.span.start..token.span.end] + .iter() + .collect(), + ); + previous_end = token.span.end; + } + gaps.push(characters[previous_end..].iter().collect()); + let (depths, partners) = bracket_structure(&texts); + let locked = vec![false; texts.len() + 1]; + Ok(Some(Self { + kinds: tokens.iter().map(|token| token.token_type).collect(), + tokens: texts, + gaps, + depths, + partners, + locked, + token_widths, + })) + } + + fn len(&self) -> usize { + self.tokens.len() + } + + fn upper(&self, index: usize) -> String { + self.tokens[index].to_ascii_uppercase() + } + + fn is(&self, index: usize, text: &str) -> bool { + self.tokens + .get(index) + .is_some_and(|token| token.eq_ignore_ascii_case(text)) + } + + fn line_ranges(&self, first: usize, last: usize) -> Vec<(usize, usize)> { + let mut ranges: Vec<(usize, usize)> = Vec::new(); + let mut start = first; + for index in first + 1..=last { + if self.gaps[index].contains('\n') { + ranges.push((start, index - 1)); + start = index; + } + } + ranges.push((start, last)); + ranges + } + + /// The first and last token of the printed line holding `index`. + fn line_of(&self, index: usize) -> (usize, usize) { + let first = (1..=index) + .rev() + .find(|&position| self.gaps[position].contains('\n')) + .unwrap_or(0); + let last = (index + 1..self.len()) + .find(|&position| self.gaps[position].contains('\n')) + .map_or(self.len() - 1, |position| position - 1); + (first, last) + } + + fn line(&self, first: usize, last: usize) -> Line { + let indentation = self.indentation(first); + let lead = self.gaps[first].rsplit('\n').next().unwrap_or_default(); + let trailing = self.gaps[last + 1].split('\n').next().unwrap_or_default(); + let commented = !lead.trim().is_empty() + || !trailing.trim().is_empty() + || (first + 1..=last).any(|index| !self.gaps[index].trim().is_empty()); + let multiline = (first..=last).any(|index| self.tokens[index].contains('\n')); + let end_columns = (!commented && !multiline).then(|| { + let mut column = indentation; + let mut ends: Vec = Vec::with_capacity(last - first + 1); + for index in first..=last { + if index > first { + column += self.gaps[index].chars().count(); + } + column += self.width(index); + ends.push(column); + } + ends + }); + Line { + first, + last, + indentation, + end_columns, + } + } + + /// Indentation of the line that starts at token `first`. + fn indentation(&self, first: usize) -> usize { + self.gaps[first] + .rsplit('\n') + .next() + .unwrap_or_default() + .chars() + .count() + } + + fn width(&self, index: usize) -> usize { + let token = &self.tokens[index]; + self.token_widths + .get(&token.to_ascii_uppercase()) + .copied() + .unwrap_or_else(|| token.chars().count()) + } + + /// Whether any gap of the tokens after `first` up to `last` holds a line break. + fn spans_lines(&self, first: usize, last: usize) -> bool { + (first + 1..=last).any(|index| self.gaps[index].contains('\n')) + } + + pub(crate) fn render(&self) -> String { + let mut output = String::new(); + for (gap, token) in self.gaps.iter().zip(&self.tokens) { + output.push_str(gap); + output.push_str(token); + } + output.push_str(&self.gaps[self.tokens.len()]); + output + } +} + +fn bracket_structure(tokens: &[String]) -> (Vec, Vec>) { + let mut depths: Vec = Vec::with_capacity(tokens.len()); + let mut partners: Vec> = vec![None; tokens.len()]; + let mut open: Vec = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + match token.as_str() { + "(" | "[" | "{" => { + depths.push(open.len()); + open.push(index); + } + ")" | "]" | "}" => { + let opener = open.pop(); + depths.push(open.len()); + if let Some(opener) = opener { + partners[opener] = Some(index); + partners[index] = Some(opener); + } + } + _ => depths.push(open.len()), + } + } + (depths, partners) +} + +fn newline(indentation: usize) -> String { + format!("\n{}", " ".repeat(indentation)) +} + +impl PrintedSql<'_> { + /// Apply the layout rules that only move existing line breaks before wrapping. + pub(crate) fn arrange_clauses(&mut self, line_width: usize) { + for index in 0..self.len() { + if self.is(index, DECODE_FUNCTION) { + self.pair_decode_arguments(index); + } + } + for index in 1..self.len() { + if CHAIN_KEYWORDS.contains(&self.upper(index).as_str()) { + self.join_chain_break_in_group(index); + } + } + for index in 0..self.len() { + if self.is(index, VALUES_KEYWORD) { + self.break_values_rows_unless_fitting(index, line_width); + } + } + for index in 0..self.len() { + if self.kinds[index] == TokenType::With { + self.separate_common_table_expressions(index); + } + } + for index in 0..self.len() { + if SINGLE_ITEM_CLAUSES.contains(&self.upper(index).as_str()) { + self.join_single_line_body(index, line_width); + } + } + } + + /// Start the next line with any binary operator whose operand spans several lines. + pub(crate) fn lead_operators_of_multiline_operands(&mut self) { + for index in 1..self.len().saturating_sub(1) { + if !LEADING_OPERATORS.contains(&self.upper(index).as_str()) + || self.gaps[index] != SINGLE_SPACE + || self.gaps[index + 1].contains('\n') + || !self.ends_operand(index - 1) + || (self.is(index, "AND") && self.follows_between(index)) + { + continue; + } + let multiline = self + .left_operand_start(index - 1) + .is_some_and(|start| self.spans_lines(start, index - 1)) + || self + .right_operand_end(index + 1) + .is_some_and(|end| self.spans_lines(index + 1, end)); + if multiline { + let (first, _) = self.line_of(index); + self.gaps[index] = newline(self.indentation(first)); + } + } + } + + /// Put the first DECODE argument on its own line and each search/result pair on one line. + fn pair_decode_arguments(&mut self, name: usize) { + let opener = name + 1; + if !self.is(opener, OPEN_PARENTHESIS) || (name > 0 && self.is(name - 1, ".")) { + return; + } + let Some(closer) = self.partners[opener] else { + return; + }; + if !self.gaps[opener + 1].contains('\n') { + return; + } + let mut argument = 0_usize; + for index in opener + 2..closer { + if self.depths[index] != self.depths[opener] + 1 || self.tokens[index - 1] != COMMA { + continue; + } + argument += 1; + if argument.is_multiple_of(2) && self.gaps[index].trim().is_empty() { + self.gaps[index] = SINGLE_SPACE.to_string(); + } + } + } + + /// Join an AND or OR line break inside a bracket group whose arguments share its line. + fn join_chain_break_in_group(&mut self, index: usize) { + if !self.gaps[index].contains('\n') || !self.gaps[index].trim().is_empty() { + return; + } + let depth = self.depths[index]; + let Some(opener) = (0..index) + .rev() + .find(|&previous| self.depths[previous] < depth) + else { + return; + }; + if self.tokens[opener] == OPEN_PARENTHESIS && !self.spans_lines(opener, index - 1) { + self.gaps[index] = SINGLE_SPACE.to_string(); + } + } + + /// Start every row of a VALUES list on its own line unless the whole list fits on its line. + fn break_values_rows_unless_fitting(&mut self, values: usize, line_width: usize) { + let rows = self.values_rows(values); + let Some(&(_, last_closer)) = rows.last() else { + return; + }; + let (first, last) = self.line_of(values); + let fits = !self.spans_lines(values, last_closer) + && self + .line(first, last) + .end_columns + .is_some_and(|ends| ends.last().is_some_and(|end| *end <= line_width)); + if fits { + return; + } + let mut indentation = self.indentation(first); + if self.gaps[values].contains('\n') { + indentation = self.indentation(values); + } else if values > 0 + && self.tokens[values - 1] == OPEN_PARENTHESIS + && let Some(closer) = self.partners[values - 1] + { + self.set_break(values, &newline(indentation + CONTINUATION_INDENT)); + self.set_break(closer, &newline(indentation)); + indentation += CONTINUATION_INDENT; + } + let row_line = newline(indentation + CONTINUATION_INDENT); + for (opener, _) in rows { + self.set_break(opener, &row_line); + } + } + + /// Separate the CTEs of the WITH at `with`, and the statement after them, by one blank line. + fn separate_common_table_expressions(&mut self, with: usize) { + let (first, _) = self.line_of(with); + let indentation = self.indentation(first); + let mut item = with + 1; + if self.is(item, "RECURSIVE") { + item += 1; + } + while let Some(end) = self.common_table_expression_end(item) { + let next = end + 1; + if self.is(next, ",") { + self.blank_line_before(next + 1, indentation); + item = next + 1; + continue; + } + if next < self.len() && !self.is(next, ";") && !self.is(next, ")") { + self.blank_line_before(next, indentation); + } + break; + } + } + + /// The last token of the CTE, or CTE macro placeholder, that starts at `item`. + fn common_table_expression_end(&self, item: usize) -> Option { + let name = self.tokens.get(item)?; + if name.to_ascii_uppercase().starts_with(SENTINEL_PREFIX) + && !self.is(item + 1, "AS") + && !self.is(item + 1, OPEN_PARENTHESIS) + { + return Some(item); + } + if !matches!( + self.kinds[item], + TokenType::Var | TokenType::Identifier | TokenType::QuotedIdentifier + ) && !name.chars().all(|c| c.is_alphanumeric() || c == '_') + { + return None; + } + let mut index = item + 1; + if self.is(index, OPEN_PARENTHESIS) { + index = self.partners[index]? + 1; + } + if !self.is(index, "AS") { + return None; + } + index += 1; + if self.is(index, "NOT") { + index += 1; + } + if self.is(index, "MATERIALIZED") { + index += 1; + } + if !self.is(index, OPEN_PARENTHESIS) { + return None; + } + self.partners[index] + } + + /// Replace the line break before `index` with exactly one blank line, keeping comments. + fn blank_line_before(&mut self, index: usize, indentation: usize) { + let gap = &self.gaps[index]; + let (trailing, rest) = gap.split_once('\n').unwrap_or((gap.as_str(), "")); + let rest = rest.trim_start(); + let margin = " ".repeat(indentation); + self.gaps[index] = if rest.is_empty() { + format!("{}\n\n{margin}", trailing.trim_end()) + } else { + format!("{}\n\n{margin}{rest}", trailing.trim_end()) + }; + } + + /// Move a clause body that is one line under its clause keyword onto the keyword line. + fn join_single_line_body(&mut self, clause: usize, line_width: usize) { + let (first, last) = self.line_of(clause); + let body = last + 1; + let is_select = self.is(clause, "SELECT"); + let modifiers_only = (clause + 1..=last).all(|index| { + SELECT_MODIFIERS.contains(&self.upper(index).as_str()) + || self.kinds[index] == TokenType::Number + || matches!(self.tokens[index].as_str(), "(" | ")" | ",") + || self.depths[index] > self.depths[clause] + }); + if body >= self.len() + || clause != first + || (!is_select && clause != last) + || (is_select && !modifiers_only) + || !self.gaps[body].contains('\n') + || !self.gaps[body].trim().is_empty() + { + return; + } + let clause_indentation = self.indentation(first); + if self.indentation(body) <= clause_indentation { + return; + } + let (_, body_last) = self.line_of(body); + if body_last + 1 < self.len() && self.indentation(body_last + 1) > clause_indentation { + return; + } + let pinned = (body + 1..=body_last).any(|index| !self.gaps[index].trim().is_empty()) + || (first..=body_last).any(|index| self.tokens[index].contains('\n')); + if pinned || (!is_select && self.has_top_level_chain(body, body_last)) { + return; + } + let mut width = clause_indentation; + for index in first..=body_last { + if index > first { + width += if index == body { + 1 + } else { + self.gaps[index].chars().count() + }; + } + width += self.width(index); + } + width += self.gaps[body_last + 1] + .split('\n') + .next() + .unwrap_or_default() + .chars() + .count(); + if width <= line_width { + self.gaps[body] = SINGLE_SPACE.to_string(); + } + } + + fn has_top_level_chain(&self, first: usize, last: usize) -> bool { + let depth = (first..=last).map(|index| self.depths[index]).min(); + (first..=last).any(|index| { + Some(self.depths[index]) == depth + && CHAIN_KEYWORDS.contains(&self.upper(index).as_str()) + && !(self.is(index, "AND") && self.follows_between(index)) + }) + } + + /// Whether the AND at `index` separates the bounds of a BETWEEN. + fn follows_between(&self, index: usize) -> bool { + let depth = self.depths[index]; + for previous in (0..index).rev() { + if self.depths[previous] < depth { + return false; + } + if self.depths[previous] > depth { + continue; + } + let upper = self.upper(previous); + if upper == BETWEEN_KEYWORD { + return true; + } + if BETWEEN_BOUNDARIES.contains(&upper.as_str()) { + return false; + } + } + false + } + + fn ends_operand(&self, index: usize) -> bool { + matches!(self.tokens[index].as_str(), ")" | "]") + || self.is(index, "END") + || matches!( + self.kinds[index], + TokenType::Var + | TokenType::Identifier + | TokenType::QuotedIdentifier + | TokenType::Number + | TokenType::String + ) + } + + /// The first token of a bracket group or CASE expression that ends at `last`. + fn left_operand_start(&self, last: usize) -> Option { + if matches!(self.tokens[last].as_str(), ")" | "]") { + return self.partners[last]; + } + if !self.is(last, "END") { + return None; + } + let depth = self.depths[last]; + let mut nested = 0_usize; + for previous in (0..last).rev() { + if self.depths[previous] != depth { + continue; + } + if self.is(previous, "END") { + nested += 1; + } else if self.is(previous, "CASE") { + if nested == 0 { + return Some(previous); + } + nested -= 1; + } + } + None + } + + /// The last token of a bracket group, call, or CASE expression that starts at `first`. + fn right_operand_end(&self, first: usize) -> Option { + if self.tokens[first] == OPEN_PARENTHESIS { + return self.partners[first]; + } + let word = self.tokens[first] + .chars() + .all(|character| character.is_alphanumeric() || character == '_'); + if word && self.is(first + 1, OPEN_PARENTHESIS) { + return self.partners[first + 1]; + } + if !self.is(first, CASE_KEYWORD) { + return None; + } + self.case_end(first) + } +} + +/// Line width and the printed widths of tokens that stand in for longer authored text. +pub(crate) struct WrapOptions<'a> { + pub(crate) line_width: usize, + pub(crate) token_widths: &'a HashMap, +} + +/// Re-break every line of `printed` that is longer than the line width and can be shortened. +pub(crate) fn wrap_lines(printed: &mut PrintedSql<'_>, line_width: usize) { + printed.lock_unbreakable_groups(); + let mut pending: Vec<(usize, usize)> = printed.line_ranges(0, printed.len() - 1); + pending.reverse(); + while let Some((first, last)) = pending.pop() { + let line = printed.line(first, last); + if !overflows(&line, line_width) || !printed.break_line(&line) { + continue; + } + let mut lines = printed.line_ranges(first, last); + lines.reverse(); + pending.extend(lines); + } +} + +fn overflows(line: &Line, line_width: usize) -> bool { + line.end_columns + .as_ref() + .is_some_and(|ends| ends.last().is_some_and(|end| *end > line_width)) +} + +impl PrintedSql<'_> { + /// Lock the inside of VALUES rows and of type parameter lists such as `NUMBER(38, 2)`. + fn lock_unbreakable_groups(&mut self) { + for index in 0..self.len() { + if self.is(index, VALUES_KEYWORD) { + for (opener, closer) in self.values_rows(index) { + self.lock(opener, closer); + } + } else if let Some(closer) = self.type_parameters_closer(index) { + self.lock(index - 1, closer); + } + } + } + + fn lock(&mut self, first: usize, last: usize) { + for gap in first + 1..=last { + self.locked[gap] = true; + } + } + + /// The bracketed rows that directly follow the VALUES keyword at `values`. + fn values_rows(&self, values: usize) -> Vec<(usize, usize)> { + let mut rows: Vec<(usize, usize)> = Vec::new(); + let mut opener = values + 1; + while opener < self.len() && self.tokens[opener] == OPEN_PARENTHESIS { + let Some(closer) = self.partners[opener] else { + break; + }; + rows.push((opener, closer)); + if !(self.is(closer + 1, COMMA) && self.is(closer + 2, OPEN_PARENTHESIS)) { + break; + } + opener = closer + 2; + } + rows + } + + /// The closer of a type parameter list opened at `opener`, such as `(38, 2)` of `NUMBER`. + fn type_parameters_closer(&self, opener: usize) -> Option { + if opener < 1 || self.tokens[opener] != OPEN_PARENTHESIS { + return None; + } + let closer = self.partners[opener]?; + let name = &self.tokens[opener - 1]; + if closer == opener + 1 || !name.chars().all(|c| c.is_alphanumeric() || c == '_') { + return None; + } + let type_position = opener > 1 && (self.is(opener - 2, "::") || self.is(opener - 2, "AS")); + if !type_position && !TYPE_NAMES.contains(&name.to_ascii_uppercase().as_str()) { + return None; + } + let parameters = (opener + 1..closer).all(|index| { + let token = &self.tokens[index]; + token == COMMA + || is_digits(token) + || TYPE_PARAMETER_WORDS.contains(&token.to_ascii_uppercase().as_str()) + }); + parameters.then_some(closer) + } + + /// Apply the outermost applicable break: chains, VALUES rows, bracket groups, then commas. + fn break_line(&mut self, line: &Line) -> bool { + let values_row = self.tokens[line.first] == OPEN_PARENTHESIS && self.locked[line.first + 1]; + !values_row + && (self.break_before(line, &CHAIN_KEYWORDS, 0) + || self.break_before(line, &ARITHMETIC_OPERATORS, CONTINUATION_INDENT) + || self.break_values_rows(line) + || self.explode_group(line) + || self.break_after_commas(line)) + } + + /// Put each row of a VALUES list that starts this line on its own line. + fn break_values_rows(&mut self, line: &Line) -> bool { + let depth = self.line_depth(line); + let Some(values) = (line.first..=line.last).find(|&index| { + self.is(index, VALUES_KEYWORD) + && (self.depths[index] == depth || index == line.first) + && !self.locked[index + 1] + }) else { + return false; + }; + let rows = self.values_rows(values); + if rows.is_empty() + || rows + .iter() + .all(|(opener, _)| self.gaps[*opener].contains('\n')) + { + return false; + } + let row_line = newline(line.indentation + CONTINUATION_INDENT); + for (opener, _) in rows { + self.gaps[opener] = row_line.clone(); + } + true + } + + /// Put the arguments or CASE branches of the outermost, widest group on their own lines. + fn explode_group(&mut self, line: &Line) -> bool { + let Some(ends) = line.end_columns.as_ref() else { + return false; + }; + let start_column = |index: usize| { + if index == line.first { + line.indentation + } else { + ends[index - 1 - line.first] + } + }; + let line_end = ends[ends.len() - 1]; + let mut chosen: Option<(usize, usize, usize)> = None; + for opener in line.first..=line.last { + let Some(closer) = self.group_closer(opener, line) else { + continue; + }; + let end_column = if closer > line.last { + line_end + } else { + ends[closer - line.first] + }; + let width = end_column - start_column(opener); + let better = chosen.is_none_or(|(best, _, best_width)| { + self.depths[opener] < self.depths[best] + || (self.depths[opener] == self.depths[best] && width >= best_width) + }); + if better { + chosen = Some((opener, closer, width)); + } + } + let Some((opener, closer, _)) = chosen else { + return false; + }; + if closer > line.last { + self.indent_lines(opener + 1, closer - 1, CONTINUATION_INDENT); + } + let inner = newline(line.indentation + CONTINUATION_INDENT); + if self.is(opener, CASE_KEYWORD) { + self.explode_case(opener, closer, line.indentation); + return true; + } + self.set_break(opener + 1, &inner); + self.set_break(closer, &newline(line.indentation)); + if self.is(opener + 1, VALUES_KEYWORD) { + return true; + } + let window = opener > 0 && self.is(opener - 1, "OVER"); + let decode = opener > 0 && self.is(opener - 1, DECODE_FUNCTION); + let mut argument = 0_usize; + for index in opener + 2..closer { + if self.depths[index] != self.depths[opener] + 1 { + continue; + } + if window { + if WINDOW_CLAUSE_KEYWORDS.contains(&self.upper(index).as_str()) { + self.set_break(index, &inner); + } + } else if self.tokens[index - 1] == COMMA { + argument += 1; + if !decode || !argument.is_multiple_of(2) { + self.set_break(index, &inner); + } + } + } + true + } + + /// The closer of an explodable group opened at `opener` on `line`. + fn group_closer(&self, opener: usize, line: &Line) -> Option { + if self.is(opener, CASE_KEYWORD) { + let end = self.case_end(opener)?; + return (end <= line.last && end > opener + 2).then_some(end); + } + let closer = self.partners[opener]?; + let explodable = closer > opener + 2 + && matches!(self.tokens[opener].as_str(), "(" | "[" | "{") + && !self.locked[opener + 1] + && (closer <= line.last || opener < line.last); + explodable.then_some(closer) + } + + /// The END of the CASE expression at `case`. + fn case_end(&self, case: usize) -> Option { + let depth = self.depths[case]; + let mut nested = 0_usize; + for index in case + 1..self.len() { + if self.depths[index] < depth { + return None; + } + if self.depths[index] != depth { + continue; + } + if self.is(index, CASE_KEYWORD) { + nested += 1; + } else if self.is(index, "END") { + if nested == 0 { + return Some(index); + } + nested -= 1; + } + } + None + } + + /// Put each WHEN, THEN and ELSE of a one-line CASE on its own line and END under CASE. + fn explode_case(&mut self, case: usize, end: usize, indentation: usize) { + let inner = newline(indentation + CONTINUATION_INDENT); + let depth = self.depths[case]; + let mut nested = 0_usize; + for index in case + 1..end { + if self.depths[index] != depth { + continue; + } + if self.is(index, CASE_KEYWORD) { + nested += 1; + } else if self.is(index, "END") { + nested -= 1; + } else if nested == 0 && CASE_BRANCH_KEYWORDS.contains(&self.upper(index).as_str()) { + self.set_break(index, &inner); + } + } + self.set_break(end, &newline(indentation)); + } + + /// Replace a whitespace-only gap; comments keep their gaps. + fn set_break(&mut self, index: usize, gap: &str) { + if self.gaps[index].trim().is_empty() { + self.gaps[index] = gap.to_string(); + } + } + + /// Indent lines starting within `first..=last` by `extra`, except gaps with block comments. + fn indent_lines(&mut self, first: usize, last: usize, extra: usize) { + let margin = format!("\n{}", " ".repeat(extra)); + for index in first..=last { + if self.gaps[index].contains('\n') && !self.gaps[index].contains("/*") { + self.gaps[index] = self.gaps[index].replace('\n', &margin); + } + } + } + + /// Break before every operator of `operators` at the line's outermost depth. + fn break_before(&mut self, line: &Line, operators: &[&str], extra_indent: usize) -> bool { + let depth = self.line_depth(line); + let mut between = false; + let mut breaks: Vec = Vec::new(); + for index in line.first..=line.last { + if self.depths[index] != depth || self.locked[index] { + continue; + } + let upper = self.upper(index); + if upper == BETWEEN_KEYWORD { + between = true; + continue; + } + if upper == BETWEEN_SEPARATOR && between { + between = false; + continue; + } + if index > line.first + && operators.contains(&upper.as_str()) + && self.gaps[index] == SINGLE_SPACE + && ends_operand(&self.tokens[index - 1]) + { + breaks.push(index); + } + } + let indentation = newline(line.indentation + extra_indent); + for index in &breaks { + self.gaps[*index] = indentation.clone(); + } + !breaks.is_empty() + } + + /// Break after every comma at the outermost comma depth of the line. + fn break_after_commas(&mut self, line: &Line) -> bool { + let Some(depth) = (line.first..line.last) + .filter(|&index| self.tokens[index] == COMMA && !self.locked[index + 1]) + .map(|index| self.depths[index]) + .min() + else { + return false; + }; + let indentation = newline(line.indentation + CONTINUATION_INDENT); + let mut changed = false; + for index in line.first..line.last { + if self.depths[index] == depth && self.tokens[index] == COMMA && !self.locked[index + 1] + { + self.gaps[index + 1] = indentation.clone(); + changed = true; + } + } + changed + } + + fn line_depth(&self, line: &Line) -> usize { + (line.first..=line.last) + .map(|index| self.depths[index]) + .min() + .unwrap_or_default() + } +} + +fn is_digits(token: &str) -> bool { + token.chars().all(|character| character.is_ascii_digit()) +} + +fn ends_operand(token: &str) -> bool { + token.chars().last().is_some_and(|character| { + character.is_alphanumeric() || matches!(character, '_' | '\'' | '"' | '`' | ')' | ']') + }) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/terminal_shape.rs b/crates/sqlclean/src/sql_lint/_helpers/terminal_shape.rs new file mode 100644 index 0000000..73637fa --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/terminal_shape.rs @@ -0,0 +1,303 @@ +use polyglot_sql::tokens::{Span, Token, TokenType}; + +const CEREMONIAL_SELECT_LITERAL: &str = "1"; + +pub(super) fn final_cte_name_spans(tokens: &[Token], depths: &[usize]) -> Vec { + let roots = root_with_indices(tokens, depths); + let mut names: Vec<(usize, bool)> = Vec::new(); + for (index, token) in tokens.iter().enumerate() { + if token.token_type != TokenType::With { + continue; + } + let depth = depths[index]; + let first = names.len(); + let mut next = index + 1; + if tokens + .get(next) + .is_some_and(|token| token.text.eq_ignore_ascii_case("recursive")) + { + next += 1; + } + loop { + let name = next; + let Some(open) = cte_body_open(tokens, depths, name) else { + break; + }; + let Some(close) = (open + 1..tokens.len()).find(|&position| { + depths[position] == depth + 1 && tokens[position].token_type == TokenType::RParen + }) else { + break; + }; + names.push((name, false)); + if tokens + .get(close + 1) + .is_none_or(|token| token.token_type != TokenType::Comma) + { + break; + } + next = close + 2; + } + if roots.contains(&index) + && names.len() > first + && let Some((_, terminal)) = names.last_mut() + { + *terminal = true; + } + } + names + .into_iter() + .filter_map(|(index, terminal)| { + let is_final = tokens[index].text.eq_ignore_ascii_case("final"); + (terminal != is_final).then_some(tokens[index].span) + }) + .collect() +} + +pub(super) fn root_with_indices(tokens: &[Token], depths: &[usize]) -> Vec { + let significant: Vec = (0..tokens.len()) + .filter(|&index| { + !matches!( + tokens[index].token_type, + TokenType::Space + | TokenType::Break + | TokenType::LineComment + | TokenType::BlockComment + ) + }) + .collect(); + significant + .split(|&index| tokens[index].token_type == TokenType::Semicolon) + .filter_map(|statement| { + root_query_tokens(tokens, depths, statement) + .first() + .copied() + }) + .filter(|&index| tokens[index].token_type == TokenType::With) + .collect() +} + +fn root_query_tokens<'a>( + tokens: &[Token], + depths: &[usize], + mut significant: &'a [usize], +) -> &'a [usize] { + while significant + .last() + .is_some_and(|&index| tokens[index].token_type == TokenType::Semicolon) + { + significant = &significant[..significant.len() - 1]; + } + while let Some((&first, rest)) = significant.split_first() { + if tokens[first].token_type != TokenType::LParen + || rest + .last() + .is_none_or(|&index| tokens[index].token_type != TokenType::RParen) + || rest.iter().any(|&index| depths[index] <= depths[first]) + { + break; + } + significant = &rest[..rest.len() - 1]; + } + significant +} + +fn cte_body_open(tokens: &[Token], depths: &[usize], name: usize) -> Option { + let mut position = name + 1; + if tokens.get(position)?.token_type == TokenType::LParen { + position = (position + 1..tokens.len()).find(|&index| { + depths[index] == depths[name] + 1 && tokens[index].token_type == TokenType::RParen + })? + 1; + } + if tokens.get(position)?.token_type != TokenType::As { + return None; + } + position += 1; + if tokens.get(position)?.token_type == TokenType::Not { + position += 1; + } + if tokens + .get(position)? + .text + .eq_ignore_ascii_case("materialized") + { + position += 1; + } + (tokens.get(position)?.token_type == TokenType::LParen).then_some(position) +} + +pub(super) fn collect_terminal_shape_facts( + tokens: &[Token], + depths: &[usize], + significant: &[usize], + allows_ceremonial_select: bool, +) -> (Vec, Vec) { + let mut cte_only_bodies: Vec = Vec::new(); + let mut terminal_selects: Vec = Vec::new(); + let significant = root_query_tokens(tokens, depths, significant); + let Some(&first) = significant.first() else { + return (cte_only_bodies, terminal_selects); + }; + let root_depth = depths[first]; + let has_top_level_with = significant.iter().any(|&index| { + depths[index] == root_depth && tokens[index].text.eq_ignore_ascii_case("with") + }); + let terminal_set_operator = significant.iter().find(|&&index| { + depths[index] == root_depth + && matches!( + tokens[index].token_type, + TokenType::Union | TokenType::Intersect | TokenType::Except + ) + }); + if let Some(&operator) = terminal_set_operator { + cte_only_bodies.push(tokens[operator].span); + if has_top_level_with { + terminal_selects.push(tokens[operator].span); + } + return (cte_only_bodies, terminal_selects); + } + let Some(root_select_position) = significant.iter().position(|&index| { + depths[index] == root_depth && tokens[index].token_type == TokenType::Select + }) else { + return (cte_only_bodies, terminal_selects); + }; + let root_select = significant[root_select_position]; + let tail: Vec = significant[root_select_position + 1..] + .iter() + .copied() + .take_while(|&index| tokens[index].token_type != TokenType::Semicolon) + .filter(|&index| depths[index] == root_depth) + .collect(); + let has_terminal_logic = tail.iter().any(|&index| { + matches!( + tokens[index].token_type, + TokenType::Join + | TokenType::Where + | TokenType::Group + | TokenType::Having + | TokenType::Qualify + | TokenType::Order + | TokenType::Limit + | TokenType::Offset + | TokenType::Union + | TokenType::Intersect + | TokenType::Except + ) + }); + if !has_top_level_with || has_terminal_logic { + cte_only_bodies.push(tokens[root_select].span); + } + if !has_top_level_with { + return (cte_only_bodies, terminal_selects); + } + + let final_cte = (0..root_select_position) + .filter(|&position| { + let index = significant[position]; + depths[index] == root_depth + && tokens[index].token_type == TokenType::As + && significant + .get(position + 1) + .is_some_and(|&next| tokens[next].token_type == TokenType::LParen) + }) + .filter_map(|position| cte_name_before_as(tokens, significant, position)) + .next_back(); + let from_position = tail + .iter() + .position(|&index| is_terminal_from(tokens, significant, index)); + let terminal_is_plain = from_position.is_some_and(|position| { + let projection = &tail[..position]; + let relation = tail.get(position + 1); + let remainder = &tail[position.saturating_add(2)..]; + !projection.is_empty() + && plain_terminal_projection(tokens, projection) + && relation.is_some_and(|&index| { + final_cte + .as_ref() + .is_some_and(|name| tokens[index].text.eq_ignore_ascii_case(name)) + }) + && remainder.is_empty() + }); + let terminal_is_ceremonial = allows_ceremonial_select + && tail.len() == 1 + && tokens[tail[0]].text == CEREMONIAL_SELECT_LITERAL; + if !terminal_is_plain && !terminal_is_ceremonial { + terminal_selects.push(tokens[root_select].span); + } + (cte_only_bodies, terminal_selects) +} + +fn cte_name_before_as( + tokens: &[Token], + significant: &[usize], + as_position: usize, +) -> Option { + let previous_position = as_position.checked_sub(1)?; + let previous = significant[previous_position]; + if is_lint_identifier(&tokens[previous]) || tokens[previous].text.eq_ignore_ascii_case("final") + { + return Some(tokens[previous].text.to_ascii_lowercase()); + } + if tokens[previous].token_type != TokenType::RParen { + return None; + } + + let mut balance = 1_usize; + for position in (0..previous_position).rev() { + let index = significant[position]; + match tokens[index].token_type { + TokenType::RParen => balance += 1, + TokenType::LParen => { + balance = balance.saturating_sub(1); + if balance == 0 { + let name = position.checked_sub(1).map(|value| significant[value])?; + return (is_lint_identifier(&tokens[name]) + || tokens[name].text.eq_ignore_ascii_case("final")) + .then(|| tokens[name].text.to_ascii_lowercase()); + } + } + _ => {} + } + } + None +} + +fn plain_terminal_projection(tokens: &[Token], projection: &[usize]) -> bool { + projection.iter().all(|&index| { + is_terminal_projection_identifier(&tokens[index]) + || matches!( + tokens[index].token_type, + TokenType::Star | TokenType::Dot | TokenType::Comma | TokenType::As + ) + }) +} + +fn is_terminal_projection_identifier(token: &Token) -> bool { + is_lint_identifier(token) + || matches!( + token.token_type, + TokenType::Comment + | TokenType::Date + | TokenType::Key + | TokenType::Sequence + | TokenType::Time + ) + || (token.token_type == TokenType::Percent && token.text.eq_ignore_ascii_case("percent")) +} + +fn is_lint_identifier(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::Identifier | TokenType::QuotedIdentifier | TokenType::Var + ) +} + +fn is_terminal_from(tokens: &[Token], significant: &[usize], index: usize) -> bool { + if tokens[index].token_type != TokenType::From { + return false; + } + significant + .iter() + .position(|&candidate| candidate == index) + .and_then(|position| position.checked_sub(1)) + .is_none_or(|position| tokens[significant[position]].token_type != TokenType::Distinct) +} diff --git a/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs b/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs new file mode 100644 index 0000000..8b5a43a --- /dev/null +++ b/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs @@ -0,0 +1,772 @@ +//! Print authored SQL tokens with whitespace taken from a parse-tree layout oracle. + +use std::collections::{BTreeMap, BTreeSet}; + +use polyglot_sql::Dialect; +use polyglot_sql::tokens::{Token, TokenType}; + +use crate::sql_lint::constants::{CLOSE_PARENTHESIS, OPEN_PARENTHESIS}; +use crate::sql_tokens::main::canonical_tokens::canonical_tokens; +use crate::sql_tokens::main::case_folding::foldable_tokens; +use crate::sql_tokens::main::is_unquoted_word::is_unquoted_word; +use crate::sql_tokens::main::name_positions::name_positions; +use crate::sql_tokens::main::token_texts::token_texts; + +const TOKEN_PRESERVATION_FAILURE: &str = + "native formatter would change authored SQL tokens, not only layout"; +const COMMENT_ATTACHMENT_FAILURE: &str = + "native formatter could not preserve comment token attachments"; +const STRING_BREAK_FAILURE: &str = + "native formatter would join adjacent string literals across a line break"; +const ALIGNMENT_FAILURE: &str = "native formatter could not align its layout with authored tokens"; +const RESYNC_WINDOW: usize = 48; +const RESYNC_CONFIRMATION: usize = 1; +const NO_SPACE_BEFORE: [&str; 7] = [",", ")", "]", ".", ";", "::", ":"]; +const NO_SPACE_AFTER: [&str; 5] = ["(", "[", ".", "::", ":"]; +const STRUCTURAL_PUNCTUATION: [&str; 6] = ["(", ")", "[", "]", ",", ";"]; + +#[derive(Debug, Clone)] +pub(crate) struct Comment { + pub(crate) start: usize, + pub(crate) end: usize, + pub(crate) text: String, + pub(crate) line: bool, + pub(crate) leading: bool, +} + +/// One authored or oracle token with its raw source text and alignment key. +struct Lexeme { + start: usize, + end: usize, + raw: String, + key: LexemeKey, + /// In the reserved-keyword and built-in fold set shared with the change fingerprint. + foldable: bool, + /// An unquoted authored word outside the name positions that keep reserved words exact. + recasable: bool, + /// An unquoted word the dialect tokenizer types as a keyword. + keyword: bool, +} + +/// How lexemes compare: words ignoring ASCII case, punctuation by type, the rest by raw text. +#[derive(Clone, Copy, PartialEq, Eq)] +enum LexemeKey { + Word, + Punctuation(TokenType), + Raw, +} + +impl Lexeme { + fn same_key(&self, other: &Lexeme) -> bool { + self.key == other.key + && match self.key { + LexemeKey::Word => self.raw.eq_ignore_ascii_case(&other.raw), + LexemeKey::Punctuation(_) => true, + LexemeKey::Raw => self.raw == other.raw, + } + } +} + +/// The source text and case-fold flag of every token, read once for the token check. +pub(crate) struct TokenTexts { + texts: Vec, + foldable: Vec, +} + +impl TokenTexts { + #[cfg(test)] + pub(crate) fn read(sql: &str, tokens: &[Token], dialect: &Dialect) -> Result { + let texts = token_texts(sql, tokens)?; + let foldable = foldable_tokens(&texts, tokens, dialect); + Ok(Self { texts, foldable }) + } + + /// Whether token `index` here, upper-cased when foldable, spells `canonical`. + fn same_canonical(&self, index: usize, canonical: &str) -> bool { + let (text, fold) = (&self.texts[index], self.foldable[index]); + text.len() == canonical.len() + && text.bytes().zip(canonical.bytes()).all(|(byte, expected)| { + expected + == if fold { + byte.to_ascii_uppercase() + } else { + byte + } + }) + } +} + +/// An authored token the printer recased to the layout guide's keyword spelling. +pub(crate) struct KeywordRecase { + pub(crate) generated: String, + pub(crate) keyword: bool, +} + +/// Comments attached to the authored token they precede or follow. +#[derive(Default)] +struct Attached<'a> { + leading: Vec<&'a Comment>, + trailing: Vec<&'a Comment>, +} + +pub(crate) fn comments_in(sql: &str, tokens: &[Token]) -> Vec { + let characters: Vec = sql.chars().collect(); + let mut comments: Vec = Vec::new(); + let mut gap_start = 0_usize; + for token in tokens { + comments.extend(comments_in_gap(&characters, gap_start, token.span.start)); + gap_start = token.span.end; + } + comments.extend(comments_in_gap(&characters, gap_start, characters.len())); + comments +} + +/// Split the non-whitespace text the tokenizer skipped between two tokens into comments. +fn comments_in_gap(characters: &[char], start: usize, end: usize) -> Vec { + let mut comments: Vec = Vec::new(); + let mut index = start; + while index < end { + if characters[index].is_whitespace() { + index += 1; + continue; + } + let start = index; + let line_start = (0..start) + .rev() + .find(|&position| characters[position] == '\n') + .map_or(0, |position| position + 1); + let leading = characters[line_start..start] + .iter() + .all(|character| character.is_whitespace()); + let block = characters[index] == '/' && characters.get(index + 1) == Some(&'*'); + index = if block { + block_comment_end(characters, start, end) + } else { + (start..end) + .find(|&position| characters[position] == '\n') + .unwrap_or(end) + }; + comments.push(Comment { + start, + end: index, + text: characters[start..index].iter().collect(), + line: !block, + leading, + }); + } + comments +} + +/// End of a block comment: nested comments when they close inside the gap, else the first `*/`. +fn block_comment_end(characters: &[char], start: usize, end: usize) -> usize { + let closes_at = |position: usize| { + position + 1 < end && characters[position] == '*' && characters[position + 1] == '/' + }; + let opens_at = |position: usize| { + position + 1 < end && characters[position] == '/' && characters[position + 1] == '*' + }; + let mut depth = 0_usize; + let mut position = start; + while position < end { + if opens_at(position) { + depth += 1; + position += 2; + } else if closes_at(position) { + depth -= 1; + position += 2; + if depth == 0 { + return position; + } + } else { + position += 1; + } + } + (start + 2..end) + .find(|&position| closes_at(position)) + .map_or(end, |position| position + 2) +} + +pub(crate) fn neutralize_comments(sql: &str, comments: &[Comment]) -> String { + let mut characters: Vec = sql.chars().collect(); + for comment in comments { + for character in &mut characters[comment.start..comment.end] { + if *character != '\n' && *character != '\r' { + *character = ' '; + } + } + } + characters.into_iter().collect() +} + +/// Authored SQL text with its dialect tokens and the comments between them. +pub(crate) struct AuthoredSql<'a> { + pub(crate) sql: &'a str, + pub(crate) tokens: &'a [Token], + pub(crate) comments: &'a [Comment], +} + +/// The lexemes and characters of a layout oracle, read once for every print against it. +pub(crate) struct OracleLayout { + generated: Vec, + characters: Vec, +} + +/// Tokenize `oracle` for printing against it. +pub(crate) fn oracle_layout(oracle: &str, dialect: &Dialect) -> Result { + let oracle_tokens = dialect + .tokenize(oracle) + .map_err(|error| error.to_string())?; + Ok(OracleLayout { + generated: lexemes(oracle, &oracle_tokens, None)?, + characters: oracle.chars().collect(), + }) +} + +/// Print every authored token in order, taking only whitespace and keyword case from `oracle`. +pub(crate) fn print_authored_tokens( + source: &AuthoredSql<'_>, + oracle: &OracleLayout, + dialect: &Dialect, +) -> Result<(String, BTreeMap, TokenTexts), String> { + let AuthoredSql { + sql, + tokens, + comments, + } = *source; + let authored = lexemes(sql, tokens, Some(dialect))?; + let generated = &oracle.generated; + let alignment = align(&authored, generated)?; + let separators = separators(&authored, generated, &alignment, &oracle.characters)?; + let attached = attach_comments(tokens, comments); + let mut output = String::with_capacity(sql.len() + sql.len() / 4); + let string_breaks = string_breaks(sql, tokens); + let mut recases: BTreeMap = BTreeMap::new(); + for (index, lexeme) in authored.iter().enumerate() { + let forced_break = string_breaks.contains(&index) && !separators[index].contains('\n'); + let forced: String; + let separator: &str = if forced_break { + forced = format!("\n{}", current_indentation(&output)); + &forced + } else { + &separators[index] + }; + let leading = &attached[index].leading; + if leading.is_empty() { + output.push_str(separator); + } else if index == 0 || separator.contains('\n') { + output.push_str(separator); + let indentation = current_indentation(&output); + for comment in leading { + output.push_str(&comment.text); + output.push('\n'); + output.push_str(&indentation); + } + } else { + let indentation = current_indentation(&output); + for comment in leading { + output.push('\n'); + output.push_str(&indentation); + output.push_str(&comment.text); + } + output.push('\n'); + output.push_str(&indentation); + } + let guide = alignment[index].map(|at| &generated[at]); + if let Some(guide) = guide.filter(|guide| confirms_keyword(lexeme, guide)) { + recases.insert( + index, + KeywordRecase { + generated: guide.raw.clone(), + keyword: guide.keyword, + }, + ); + } + output.push_str(&printed_text(lexeme, guide)); + let trailing = &attached[index].trailing; + for (position, comment) in trailing.iter().enumerate() { + output.push(' '); + output.push_str(&comment.text); + if !comment.line { + continue; + } + let next_starts_line = if position + 1 < trailing.len() { + false + } else { + separators + .get(index + 1) + .is_none_or(|next| next.starts_with('\n')) + }; + if !next_starts_line { + output.push('\n'); + if position + 1 < trailing.len() { + output.push_str(¤t_indentation(&output)); + } + } + } + } + let mut texts = TokenTexts { + texts: Vec::with_capacity(authored.len()), + foldable: Vec::with_capacity(authored.len()), + }; + for lexeme in authored { + texts.texts.push(lexeme.raw); + texts.foldable.push(lexeme.foldable); + } + Ok((output, recases, texts)) +} + +/// Refuse output that changes anything but layout, fold-set case, and recorded keyword recases. +pub(crate) fn verify_printed_tokens( + (before, before_tokens, before_texts): (&str, &[Token], &TokenTexts), + (after, after_tokens): (&str, &[Token]), + dialect: &Dialect, + recases: &BTreeMap, +) -> Result<(), String> { + let after_canonical = canonical_tokens(after, after_tokens, dialect)?; + if before_texts.texts.len() != after_canonical.len() { + return Err(TOKEN_PRESERVATION_FAILURE.to_string()); + } + let mut after_texts: Option> = None; + for (index, canonical) in after_canonical.iter().enumerate() { + if before_texts.same_canonical(index, &canonical.text) { + continue; + } + let Some(recase) = recases.get(&index) else { + return Err(TOKEN_PRESERVATION_FAILURE.to_string()); + }; + let texts: &Vec = match after_texts { + Some(ref texts) => texts, + None => after_texts.insert(token_texts(after, after_tokens)?), + }; + let printed: &str = &texts[index]; + if !recase.allows(&before_texts.texts[index], printed) { + return Err(TOKEN_PRESERVATION_FAILURE.to_string()); + } + } + if comment_positions(before, before_tokens) != comment_positions(after, after_tokens) { + return Err(COMMENT_ATTACHMENT_FAILURE.to_string()); + } + if !string_breaks(before, before_tokens).is_subset(&string_breaks(after, after_tokens)) { + return Err(STRING_BREAK_FAILURE.to_string()); + } + Ok(()) +} + +impl KeywordRecase { + /// Whether `authored` became `printed` only by taking the guide's upper-case keyword spelling. + fn allows(&self, authored: &str, printed: &str) -> bool { + self.keyword + && printed == self.generated + && printed == printed.to_ascii_uppercase() + && authored.eq_ignore_ascii_case(printed) + } +} + +pub(crate) fn tokenize(sql: &str, dialect: &Dialect) -> Result, String> { + dialect.tokenize(sql).map_err(|error| error.to_string()) +} + +fn comment_positions(sql: &str, tokens: &[Token]) -> Vec<(String, usize)> { + let mut positions: Vec<(String, usize)> = Vec::new(); + for comment in comments_in(sql, tokens) { + let preceding = tokens.partition_point(|token| token.span.end <= comment.start); + positions.push((comment.text, preceding)); + } + positions +} + +/// Lexemes of `sql`; only authored lexemes carry the recase classification. +fn lexemes(sql: &str, tokens: &[Token], dialect: Option<&Dialect>) -> Result, String> { + let raws = token_texts(sql, tokens)?; + let (foldable, recasable) = match dialect { + Some(dialect) => { + let uppers: Vec = raws.iter().map(|raw| raw.to_ascii_uppercase()).collect(); + let names = name_positions(&uppers); + let foldable = foldable_tokens(&raws, tokens, dialect); + let recasable: Vec = raws + .iter() + .zip(names) + .map(|(raw, name)| !name && is_unquoted_word(raw)) + .collect(); + (foldable, recasable) + } + None => (vec![false; tokens.len()], vec![false; tokens.len()]), + }; + let mut lexemes = Vec::with_capacity(tokens.len()); + for (index, (raw, token)) in raws.into_iter().zip(tokens).enumerate() { + lexemes.push(lexeme(raw, token, foldable[index], recasable[index])); + } + Ok(lexemes) +} + +fn lexeme(raw: String, token: &Token, foldable: bool, recasable: bool) -> Lexeme { + let word = is_unquoted_word(&raw); + let key = if word { + LexemeKey::Word + } else if raw + .chars() + .all(|character| character.is_ascii_punctuation()) + { + LexemeKey::Punctuation(token.token_type) + } else { + LexemeKey::Raw + }; + let keyword = word + && !matches!( + token.token_type, + TokenType::Var | TokenType::Identifier | TokenType::QuotedIdentifier + ); + Lexeme { + start: token.span.start, + end: token.span.end, + raw, + key, + foldable, + recasable, + keyword, + } +} + +fn is_string_literal(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::String + | TokenType::NationalString + | TokenType::EscapeString + | TokenType::RawString + | TokenType::ByteString + | TokenType::UnicodeString + | TokenType::HexString + | TokenType::BitString + | TokenType::DollarString + | TokenType::TripleSingleQuotedString + | TokenType::TripleDoubleQuotedString + ) +} + +/// Indices of string literals that follow another string literal across a line break. +fn string_breaks(sql: &str, tokens: &[Token]) -> BTreeSet { + let characters: Vec = sql.chars().collect(); + (1..tokens.len()) + .filter(|&index| { + is_string_literal(&tokens[index - 1]) + && is_string_literal(&tokens[index]) + && characters[tokens[index - 1].span.end..tokens[index].span.start].contains(&'\n') + }) + .collect() +} + +/// Map authored tokens to matching oracle tokens; a closer only matches its opener's partner. +fn align(authored: &[Lexeme], generated: &[Lexeme]) -> Result>, String> { + let mut state = Alignment { + authored, + generated, + authored_partners: bracket_partners(authored), + generated_partners: bracket_partners(generated), + forward: vec![None; authored.len()], + backward: vec![None; generated.len()], + }; + let (mut left, mut right) = (0_usize, 0_usize); + while left < authored.len() && right < generated.len() { + if state.matches(left, right) { + state.forward[left] = Some(right); + state.backward[right] = Some(left); + left += 1; + right += 1; + continue; + } + let (skip_left, skip_right) = state + .resync(left, right) + .ok_or_else(|| ALIGNMENT_FAILURE.to_string())?; + left += skip_left; + right += skip_right; + } + Ok(state.forward) +} + +struct Alignment<'a> { + authored: &'a [Lexeme], + generated: &'a [Lexeme], + authored_partners: Vec>, + generated_partners: Vec>, + forward: Vec>, + backward: Vec>, +} + +impl Alignment<'_> { + fn matches(&self, left: usize, right: usize) -> bool { + if !self.authored[left].same_key(&self.generated[right]) { + return false; + } + if !is_closer(&self.authored[left].raw) { + return true; + } + match (self.authored_partners[left], self.generated_partners[right]) { + (Some(opener), Some(generated_opener)) => match self.forward[opener] { + Some(aligned) => aligned == generated_opener, + None => self.backward[generated_opener].is_none(), + }, + (None, None) => true, + _ => false, + } + } + + fn resync(&self, left: usize, right: usize) -> Option<(usize, usize)> { + let remaining_left = self.authored.len() - left; + let remaining_right = self.generated.len() - right; + for cost in 1..=(2 * RESYNC_WINDOW) { + for skip_left in 0..=cost.min(RESYNC_WINDOW) { + let skip_right = cost - skip_left; + if skip_right > RESYNC_WINDOW { + continue; + } + if skip_left >= remaining_left || skip_right >= remaining_right { + if skip_left >= remaining_left && skip_right >= remaining_right { + return Some((remaining_left, remaining_right)); + } + continue; + } + if !self.matches(left + skip_left, right + skip_right) { + continue; + } + let anchor = &self.authored[left + skip_left]; + let confirmations = if matches!(anchor.key, LexemeKey::Punctuation(_)) { + RESYNC_CONFIRMATION + } else { + 0 + }; + let confirmed = (1..=confirmations).all(|offset| { + match ( + self.authored.get(left + skip_left + offset), + self.generated.get(right + skip_right + offset), + ) { + (Some(before), Some(after)) => before.same_key(after), + _ => true, + } + }); + if confirmed { + return Some((skip_left, skip_right)); + } + } + } + None + } +} + +fn is_closer(raw: &str) -> bool { + matches!(raw, ")" | "]" | "}") +} + +fn bracket_partners(lexemes: &[Lexeme]) -> Vec> { + let mut partners: Vec> = vec![None; lexemes.len()]; + let mut open: Vec = Vec::new(); + for (index, lexeme) in lexemes.iter().enumerate() { + match lexeme.raw.as_str() { + "(" | "[" | "{" => open.push(index), + ")" | "]" | "}" => { + if let Some(opener) = open.pop() { + partners[opener] = Some(index); + partners[index] = Some(opener); + } + } + _ => {} + } + } + partners +} + +fn separators( + authored: &[Lexeme], + generated: &[Lexeme], + alignment: &[Option], + oracle: &[char], +) -> Result, String> { + let mut separators: Vec = vec![String::new(); authored.len()]; + let anchors: Vec = (0..authored.len()) + .filter(|&index| alignment[index].is_some()) + .collect(); + if anchors.is_empty() { + return Err(ALIGNMENT_FAILURE.to_string()); + } + let gap = |left: usize, right: usize| -> String { + oracle[generated[left].end..generated[right].start] + .iter() + .collect() + }; + for index in 1..anchors[0] { + separators[index] = default_space(&authored[index - 1], &authored[index]).to_string(); + } + if anchors[0] > 0 { + separators[anchors[0]] = + default_space(&authored[anchors[0] - 1], &authored[anchors[0]]).to_string(); + } + for pair in anchors.windows(2) { + let (previous, next) = (pair[0], pair[1]); + let (previous_at, next_at) = ( + alignment[previous].ok_or(ALIGNMENT_FAILURE)?, + alignment[next].ok_or(ALIGNMENT_FAILURE)?, + ); + let merged = merged_gap( + &(previous_at..next_at) + .map(|at| gap(at, at + 1)) + .collect::>(), + ); + if previous + 1 == next { + separators[next] = merged; + continue; + } + for index in previous + 2..next { + separators[index] = default_space(&authored[index - 1], &authored[index]).to_string(); + } + let first = previous + 1; + let last = next - 1; + let substitution = next_at > previous_at + 1; + separators[first] = if default_space(&authored[previous], &authored[first]).is_empty() { + String::new() + } else if substitution { + gap(previous_at, previous_at + 1) + } else { + " ".to_string() + }; + separators[next] = if substitution { + let oracle_gap = (previous_at + 1..next_at) + .rev() + .find(|&at| generated[at].same_key(&authored[last])) + .map_or_else(|| gap(next_at - 1, next_at), |at| gap(at, at + 1)); + let unspaced = default_space(&authored[last], &authored[next]).is_empty(); + if unspaced && !oracle_gap.contains('\n') { + String::new() + } else { + oracle_gap + } + } else if merged.contains('\n') { + merged + } else { + default_space(&authored[last], &authored[next]).to_string() + }; + } + let last_anchor = anchors[anchors.len() - 1]; + for index in last_anchor + 1..authored.len() { + separators[index] = default_space(&authored[index - 1], &authored[index]).to_string(); + } + let last_at = alignment[last_anchor].ok_or(ALIGNMENT_FAILURE)?; + if last_anchor + 1 < authored.len() + && last_at + 1 < generated.len() + && !separators[last_anchor + 1].is_empty() + { + separators[last_anchor + 1] = gap(last_at, last_at + 1); + } + for index in 1..authored.len() { + let adjacent_in_source = authored[index - 1].end == authored[index].start; + let oracle_pair = matches!( + (alignment[index - 1], alignment[index]), + (Some(previous), Some(next)) if previous + 1 == next + ); + let structural = [&authored[index - 1], &authored[index]] + .iter() + .any(|lexeme| STRUCTURAL_PUNCTUATION.contains(&lexeme.raw.as_str())); + let call = + is_unquoted_word(&authored[index - 1].raw) && authored[index].raw == OPEN_PARENTHESIS; + let inside_parentheses = + authored[index - 1].raw == OPEN_PARENTHESIS || authored[index].raw == CLOSE_PARENTHESIS; + if (adjacent_in_source && !oracle_pair && (!structural || call)) + || (inside_parentheses && !separators[index].contains('\n')) + { + separators[index].clear(); + } else if separators[index].is_empty() + && !adjacent_in_source + && would_fuse(&authored[index - 1], &authored[index]) + { + separators[index] = " ".to_string(); + } + } + Ok(separators) +} + +fn merged_gap(gaps: &[String]) -> String { + if let [only] = gaps { + return only.clone(); + } + if let Some(newline) = gaps.iter().rev().find(|gap| gap.contains('\n')) { + return newline.clone(); + } + if gaps.iter().any(|gap| !gap.is_empty()) { + " ".to_string() + } else { + String::new() + } +} + +fn default_space(previous: &Lexeme, next: &Lexeme) -> &'static str { + if NO_SPACE_BEFORE.contains(&next.raw.as_str()) + || NO_SPACE_AFTER.contains(&previous.raw.as_str()) + { + "" + } else { + " " + } +} + +/// Return whether printing two separated tokens without a space could lex them differently. +fn would_fuse(previous: &Lexeme, next: &Lexeme) -> bool { + let (Some(left), Some(right)) = (previous.raw.chars().last(), next.raw.chars().next()) else { + return false; + }; + let word = |character: char| { + character.is_alphanumeric() || matches!(character, '_' | '$' | '\'' | '"' | '`' | '@') + }; + let operator = |character: char| "+-*/<>=!|&^%~:#?".contains(character); + (word(left) && word(right)) || (operator(left) && operator(right)) +} + +/// The authored text, recased only for keywords and built-in function names. +fn printed_text(authored: &Lexeme, generated: Option<&Lexeme>) -> String { + if let Some(generated) = generated.filter(|generated| confirms_keyword(authored, generated)) { + return generated.raw.clone(); + } + if !authored.foldable { + return authored.raw.clone(); + } + match generated { + Some(generated) if generated.raw.eq_ignore_ascii_case(&authored.raw) => { + generated.raw.clone() + } + Some(_) => authored.raw.clone(), + None => authored.raw.to_ascii_uppercase(), + } +} + +/// Whether the layout guide prints the aligned authored word as an upper-case keyword. +fn confirms_keyword(authored: &Lexeme, generated: &Lexeme) -> bool { + !authored.foldable + && authored.recasable + && generated.keyword + && generated.raw != authored.raw + && generated.raw == generated.raw.to_ascii_uppercase() + && generated.raw.eq_ignore_ascii_case(&authored.raw) +} + +fn attach_comments<'a>(tokens: &[Token], comments: &'a [Comment]) -> Vec> { + let mut attached: Vec> = (0..tokens.len()).map(|_| Attached::default()).collect(); + for comment in comments { + let next = tokens.partition_point(|token| token.span.start < comment.end); + let preceding = tokens.partition_point(|token| token.span.end <= comment.start); + let next = (next < tokens.len()).then_some(next); + let previous = preceding.checked_sub(1); + match (comment.leading, next, previous) { + (true, Some(next), _) | (false, Some(next), None) => { + attached[next].leading.push(comment); + } + (_, _, Some(previous)) => attached[previous].trailing.push(comment), + (_, None, None) => {} + } + } + attached +} + +fn current_indentation(output: &str) -> String { + let line = output.rsplit('\n').next().unwrap_or_default(); + line.chars() + .take_while(|character| *character == ' ' || *character == '\t') + .collect() +} diff --git a/crates/sqlclean/src/sql_lint/constants.rs b/crates/sqlclean/src/sql_lint/constants.rs new file mode 100644 index 0000000..4b329c0 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/constants.rs @@ -0,0 +1,61 @@ +pub(crate) const CAST_TYPE_SEPARATOR_KEYWORD: &str = "AS"; +pub(crate) const CLOSE_PARENTHESIS: &str = ")"; +pub const LINT_API_VERSION: u32 = 1; +pub(crate) const OPEN_PARENTHESIS: &str = "("; +pub(crate) const POSITION_ARGUMENT_SEPARATOR: &str = ","; +pub(crate) const CTE_MACRO_PLACEHOLDER_LITERAL: &str = "1"; +pub(crate) const POSTFIX_CAST_PREFIX_TOKEN_COUNT: usize = 2; +pub(crate) const RELATION_MODIFIER_KEYWORDS: [&str; 43] = [ + "ANTI", + "APPLY", + "ASOF", + "AT", + "BEFORE", + "CHANGES", + "CONNECT", + "CROSS", + "EXCEPT", + "FETCH", + "FINAL", + "FOR", + "FULL", + "GROUP", + "HAVING", + "INNER", + "INTERSECT", + "JOIN", + "LATERAL", + "LEFT", + "LIMIT", + "MATCH_RECOGNIZE", + "MINUS", + "NATURAL", + "OFFSET", + "ON", + "ORDER", + "OUTER", + "PASTE", + "PIVOT", + "POSITIONAL", + "QUALIFY", + "RETURNING", + "RIGHT", + "SAMPLE", + "SEMI", + "START", + "TABLESAMPLE", + "UNION", + "UNPIVOT", + "USING", + "WHERE", + "WINDOW", +]; +/// The parts of a `SQC038` message that names its joined relation and CTE. +pub(crate) const CROSS_JOIN_PREFIX: &str = "CROSS JOIN with `"; +pub(crate) const CROSS_JOIN_CTE_MARKER: &str = "` in CTE `"; +pub(crate) const CROSS_JOIN_SUFFIX: &str = "` creates an explicit cartesian product"; +/// The `SQC038` message for a join whose relation cannot be named. +pub(crate) const CROSS_JOIN_UNNAMED_MESSAGE: &str = "CROSS JOIN creates an explicit cartesian product with an unnamed relation; only a file-wide exception (rule and path) can cover it"; +/// Stands for the finding's project-relative path until the caller that knows it fills it in. +pub(crate) const CROSS_JOIN_PATH_PLACEHOLDER: &str = ""; +pub(crate) const CROSS_JOIN_REASON_PLACEHOLDER: &str = ""; diff --git a/crates/sqlclean/src/sql_lint/main/batch_engine.rs b/crates/sqlclean/src/sql_lint/main/batch_engine.rs new file mode 100644 index 0000000..2d7de06 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/batch_engine.rs @@ -0,0 +1,19 @@ +use crate::sql_lint::_helpers::engine::lint; +use crate::sql_lint::models::{LintRequest, LintResponse}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; + +pub fn lint_batch_json(request_json: &str) -> Result { + let requests: Vec = + serde_json::from_str(request_json).map_err(|error| error.to_string())?; + if requests.is_empty() { + return Ok("[]".to_owned()); + } + let pool = rayon::ThreadPoolBuilder::new() + .num_threads(requests.len().min(4)) + .stack_size(16 * 1024 * 1024) + .build() + .map_err(|error| error.to_string())?; + let results: Vec> = + pool.install(|| requests.into_par_iter().map(lint).collect()); + serde_json::to_string(&results).map_err(|error| error.to_string()) +} diff --git a/crates/sqlclean/src/sql_lint/main/catalogue.rs b/crates/sqlclean/src/sql_lint/main/catalogue.rs new file mode 100644 index 0000000..7b534d4 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/catalogue.rs @@ -0,0 +1,6 @@ +use crate::sql_lint::_helpers::engine::rule_metadata; +use crate::sql_lint::models::LintRuleMetadata; + +pub fn catalogue() -> Vec { + rule_metadata() +} diff --git a/crates/sqlclean/src/sql_lint/main/cross_join_subject.rs b/crates/sqlclean/src/sql_lint/main/cross_join_subject.rs new file mode 100644 index 0000000..524a580 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/cross_join_subject.rs @@ -0,0 +1,15 @@ +//! The relation and CTE a `SQC038` finding names, for relation-level Rule exceptions. + +use crate::sql_lint::constants::{CROSS_JOIN_CTE_MARKER, CROSS_JOIN_PREFIX, CROSS_JOIN_SUFFIX}; + +/// The `(relation, cte)` a cross join finding message names, `None` for the generic message. +#[must_use] +pub fn cross_join_subject(message: &str) -> Option<(String, Option)> { + let named: &str = message + .strip_prefix(CROSS_JOIN_PREFIX)? + .strip_suffix(CROSS_JOIN_SUFFIX)?; + match named.split_once(CROSS_JOIN_CTE_MARKER) { + Some((relation, cte)) => Some((relation.to_owned(), Some(cte.to_owned()))), + None => Some((named.to_owned(), None)), + } +} diff --git a/crates/sqlclean/src/sql_lint/main/default_rules.rs b/crates/sqlclean/src/sql_lint/main/default_rules.rs new file mode 100644 index 0000000..33d1cd1 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/default_rules.rs @@ -0,0 +1,6 @@ +//! The built-in Rules a family selector or an absent selection enables. + +/// The codes enabled when no Rule selection is given. +pub fn default_rules() -> Vec<&'static str> { + crate::sql_lint::_helpers::engine::DEFAULT_RULES.to_vec() +} diff --git a/crates/sqlclean/src/sql_lint/main/engine.rs b/crates/sqlclean/src/sql_lint/main/engine.rs new file mode 100644 index 0000000..2533f4f --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/engine.rs @@ -0,0 +1,5 @@ +use crate::sql_lint::_helpers::engine::lint_json_impl; + +pub fn lint_json(request_json: &str) -> Result { + lint_json_impl(request_json) +} diff --git a/crates/sqlclean/src/sql_lint/main/fill_finding_path.rs b/crates/sqlclean/src/sql_lint/main/fill_finding_path.rs new file mode 100644 index 0000000..d5a6580 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/fill_finding_path.rs @@ -0,0 +1,9 @@ +//! `fill_finding_path`: a SQL finding's help with its file's path written into the entry it prints. + +use crate::sql_lint::constants::CROSS_JOIN_PATH_PLACEHOLDER; + +/// `remediation` with the path placeholder of a printed exception entry replaced by `path`. +#[must_use] +pub fn fill_finding_path(remediation: &str, path: &str) -> String { + remediation.replace(CROSS_JOIN_PATH_PLACEHOLDER, path) +} diff --git a/crates/sqlclean/src/sql_lint/main/format_requests.rs b/crates/sqlclean/src/sql_lint/main/format_requests.rs new file mode 100644 index 0000000..286bebd --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/format_requests.rs @@ -0,0 +1,8 @@ +//! Typed, in-process batch formatting of neutralized SQL bodies. + +use crate::sql_lint::models::{FormatRequest, FormatResponse}; + +/// Format every request in parallel and return the responses in request order. +pub fn format_requests(requests: Vec) -> Vec { + crate::sql_lint::_helpers::formatter::format_requests(requests) +} diff --git a/crates/sqlclean/src/sql_lint/main/formatter.rs b/crates/sqlclean/src/sql_lint/main/formatter.rs new file mode 100644 index 0000000..42da9c9 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/formatter.rs @@ -0,0 +1,5 @@ +use crate::sql_lint::_helpers::formatter::format_json_impl; + +pub fn format_json(request_json: &str) -> Result { + format_json_impl(request_json) +} diff --git a/crates/sqlclean/src/sql_lint/main/lint.rs b/crates/sqlclean/src/sql_lint/main/lint.rs new file mode 100644 index 0000000..ab3be0b --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/lint.rs @@ -0,0 +1,8 @@ +//! Typed, in-process lint of one SQL text. + +use crate::sql_lint::models::{LintRequest, LintResponse}; + +/// Lint `request.sql` with the selected built-in Rules and return findings in source order. +pub fn lint(request: LintRequest) -> Result { + crate::sql_lint::_helpers::engine::lint(request) +} diff --git a/crates/sqlclean/src/sql_lint/main/mod.rs b/crates/sqlclean/src/sql_lint/main/mod.rs new file mode 100644 index 0000000..021ca2d --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/mod.rs @@ -0,0 +1,11 @@ +pub mod batch_engine; +pub mod catalogue; +pub mod cross_join_subject; +pub mod default_rules; +pub mod engine; +pub mod fill_finding_path; +pub mod format_requests; +pub mod formatter; +pub mod lint; +pub mod parse_sql; +pub mod validate_dialect; diff --git a/crates/sqlclean/src/sql_lint/main/parse_sql.rs b/crates/sqlclean/src/sql_lint/main/parse_sql.rs new file mode 100644 index 0000000..14408c0 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/parse_sql.rs @@ -0,0 +1,8 @@ +//! Tokenize and parse SQL the way every lint request does. + +use crate::sql_lint::models::ParsedSql; + +/// Parse `sql` under `dialect`, rejecting statements the parser kept as opaque text. +pub fn parse_sql(sql: &str, dialect: &str) -> Result { + crate::sql_lint::_helpers::parsing::parse(sql, dialect) +} diff --git a/crates/sqlclean/src/sql_lint/main/validate_dialect.rs b/crates/sqlclean/src/sql_lint/main/validate_dialect.rs new file mode 100644 index 0000000..6fe0097 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/main/validate_dialect.rs @@ -0,0 +1,13 @@ +//! Check a dialect name against the dialects the parser supports. + +use std::str::FromStr; + +use polyglot_sql::DialectType; + +/// `Ok` when `name` names a supported SQL dialect, such as `duckdb` or `snowflake`. +pub fn validate_dialect(name: &str) -> Result<(), String> { + match DialectType::from_str(name) { + Ok(_) => Ok(()), + Err(error) => Err(format!("unknown SQL dialect '{name}': {error}")), + } +} diff --git a/crates/sqlclean/src/sql_lint/mod.rs b/crates/sqlclean/src/sql_lint/mod.rs new file mode 100644 index 0000000..e3460e8 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/mod.rs @@ -0,0 +1,9 @@ +//! SQL lint Rules and the deterministic formatter over the Polyglot parser. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean/src/sql_lint/models.rs b/crates/sqlclean/src/sql_lint/models.rs new file mode 100644 index 0000000..1011345 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/models.rs @@ -0,0 +1,188 @@ +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + +use polyglot_sql::tokens::{Span, Token}; +use polyglot_sql::{DialectType, Expression}; +use serde::{Deserialize, Serialize}; + +/// SQL text tokenized and parsed under one dialect. +#[derive(Debug)] +pub struct ParsedSql { + pub dialect: DialectType, + pub tokens: Vec, + pub statements: Vec, +} + +#[derive(Debug, Default)] +pub(crate) struct QueryFacts { + pub null_comparisons: Vec, + pub implicit_cartesian_joins: Vec, + pub joins_without_condition: Vec, + pub unordered_limits: Vec, + pub redundant_distincts: Vec, + pub positional_set_stars: Vec, + pub unused_cte_names: Vec, + pub cross_joins: Vec, + pub cte_only_bodies: Vec, + pub terminal_selects: Vec, + pub nested_ctes: Vec, + pub recursive_ctes: Vec, + pub invalid_comment_attachments: Vec, + pub inline_query_relations: Vec, + pub additional: AdditionalQueryFacts, +} + +#[derive(Debug, Default)] +pub(crate) struct AdditionalQueryFacts { + pub bare_unions: Vec, + pub duplicate_table_aliases: Vec, + pub duplicate_output_aliases: Vec, + pub mixed_group_order_references: Vec, + pub redundant_else_nulls: Vec, + pub parenthesized_distincts: Vec, + pub constant_predicates: Vec, + pub consecutive_semicolons: Vec, + pub redundant_self_aliases: Vec, + pub count_one_literals: Vec, + pub unstable_row_numbers: Vec, + pub null_not_in_predicates: Vec, + pub set_arity_mismatches: Vec, + pub projected_stars: Vec, + pub unaliased_calculations: Vec, + pub unused_table_aliases: Vec, + pub null_rejected_left_joins: Vec, + pub implicit_inner_joins: Vec, + pub ambiguous_order_directions: Vec, + pub unqualified_multi_source_columns: Vec, + pub inconsistent_single_source_qualification: Vec, + pub unknown_relation_qualifiers: Vec, + pub simple_boolean_cases: Vec, + pub nested_else_cases: Vec, + pub unused_joined_relations: Vec, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct LintRequest { + pub version: u32, + pub sql: String, + pub dialect: String, + #[serde(default)] + pub enabled_rules: Option>, + #[serde(default)] + pub ignored_rules: Vec, + #[serde(default)] + pub external_identifiers: Vec, + #[serde(default)] + pub dependency_identifiers: Vec, + #[serde(default)] + pub externally_referenced_ctes: Vec, + #[serde(default)] + pub allows_ceremonial_select: bool, + #[serde(default)] + pub allows_dynamic_output_star: bool, + #[serde(default)] + pub allows_empty_fixture_star: bool, + #[serde(default)] + pub max_literal_length: Option, + #[serde(default)] + pub max_ranking_order_by: Option, + #[serde(default)] + pub relation_keys: HashMap>>, + #[serde(default)] + pub relation_names: HashMap, + /// The SQL each `@sql.` call in the query generated, so column qualification is judged as the macro wrote it. + #[serde(default)] + pub sql_macro_outputs: Vec, + /// The configuration file remediation text names; SQLClean's own when absent. + #[serde(default)] + pub config_file: Option, +} + +/// The character range one `@sql.` call's output fills, and the names its arguments pass, which a plain slot read such as `@rel.amount` expands to qualify with. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq)] +pub struct SqlMacroOutput { + pub start: usize, + pub end: usize, + #[serde(default)] + pub relations: Vec, +} + +#[derive(Debug)] +pub(crate) struct AdditionalFactOptions<'a> { + pub external_identifiers: &'a HashSet, + pub dependency_identifiers: &'a HashSet, + pub sql_macro_outputs: &'a [SqlMacroOutput], + pub allows_ceremonial_select: bool, + pub allows_dynamic_output_star: bool, + pub allows_empty_fixture_star: bool, +} + +#[derive(Debug, Serialize)] +pub struct LintResponse { + pub version: u32, + pub diagnostics: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct LintDiagnostic { + pub code: &'static str, + pub message: Cow<'static, str>, + pub remediation: Cow<'static, str>, + pub start: usize, + pub end: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub fix: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub fix_unavailable_reason: Option<&'static str>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct LintEdit { + pub start: usize, + pub end: usize, + pub replacement: String, +} + +#[derive(Debug, Clone, Copy)] +pub struct LintRuleMetadata { + pub code: &'static str, + pub message: &'static str, + pub remediation: &'static str, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct FormatRequest { + pub version: u32, + pub sql: String, + pub dialect: String, + #[serde(default)] + pub max_function_call_depth: Option, + #[serde(default)] + pub line_width: Option, + #[serde(default)] + pub token_widths: HashMap, +} + +#[derive(Debug, Serialize)] +pub struct FormatResponse { + pub version: u32, + pub sql: String, + pub changed: bool, + pub formatted: bool, + #[serde(skip_serializing_if = "Option::is_none")] + pub reason: Option, +} + +/// One lint request's explicit cross joins, with what is known about the relations they read. +#[derive(Debug)] +pub(crate) struct CrossJoinRequest<'a> { + pub(crate) statements: &'a [Expression], + pub(crate) tokens: &'a [Token], + pub(crate) cross_spans: &'a [Span], + /// Declared keys by dependency sentinel; the empty key proves at most one row. + pub(crate) relation_keys: &'a HashMap>>, + /// The relation name each dependency sentinel stands for. + pub(crate) relation_names: &'a HashMap, +} diff --git a/crates/sqlclean/src/sql_lint/tests.rs b/crates/sqlclean/src/sql_lint/tests.rs new file mode 100644 index 0000000..dde04c7 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests.rs @@ -0,0 +1,34 @@ +#[path = "tests/test_authored_tokens.rs"] +mod authored_tokens; +#[path = "tests/test_batch_lint.rs"] +mod batch_lint; +#[path = "tests/test_comment_formatter.rs"] +mod comment_formatter; +#[path = "tests/test_cross_joins.rs"] +mod cross_joins; +#[path = "tests/test_empty_fixture_lint.rs"] +mod empty_fixture_lint; +#[path = "tests/test_format_corpus.rs"] +mod format_corpus; +#[path = "tests/test_function_call_spacing.rs"] +mod function_call_spacing; +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_idempotency_reprint.rs"] +mod idempotency_reprint; +#[path = "tests/test_layout_rules.rs"] +mod layout_rules; +#[path = "tests/test_line_wrap.rs"] +mod line_wrap; +#[path = "tests/test_lint_exemptions.rs"] +mod lint_exemptions; +#[path = "tests/test_set_operation_formatter.rs"] +mod set_operation_formatter; +#[path = "tests/test_sql_lint.rs"] +mod sql_lint; +#[path = "tests/test_types.rs"] +mod test_types; +#[path = "tests/test_unused_table_alias.rs"] +mod unused_table_alias; +#[path = "tests/test_values_formatter.rs"] +mod values_formatter; diff --git a/crates/sqlclean/src/sql_lint/tests/fixtures/FORMAT_CORPUS_NOTICE.md b/crates/sqlclean/src/sql_lint/tests/fixtures/FORMAT_CORPUS_NOTICE.md new file mode 100644 index 0000000..39e0868 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/fixtures/FORMAT_CORPUS_NOTICE.md @@ -0,0 +1,32 @@ +# Formatter corpus notice + +`format_corpus.json` contains SELECT and WITH queries taken from the SQLGlot dialect identity test +fixtures for BigQuery, Databricks, DuckDB, PostgreSQL, Snowflake and T-SQL, as extracted by the +Polyglot project. One literal was reworded. The expected `formatted` and `previously_formatted` +values are SQLBuild formatter output. + +The queries are used under the following license: + +```text +MIT License + +Copyright (c) 2026 Toby Mao + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +``` diff --git a/crates/sqlclean/src/sql_lint/tests/fixtures/format_corpus.json b/crates/sqlclean/src/sql_lint/tests/fixtures/format_corpus.json new file mode 100644 index 0000000..b45dd71 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/fixtures/format_corpus.json @@ -0,0 +1,5354 @@ +[ + { + "dialect": "bigquery", + "sql": "SELECT 'foo' 'bar'", + "formatted": "SELECT 'foo' 'bar'" + }, + { + "dialect": "bigquery", + "sql": "SELECT 'foo'/* c */'bar'", + "formatted": "SELECT\n 'foo' /* c */ 'bar'" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `domain.com:project-id.mydataset.mytable`", + "formatted": "SELECT *\nFROM `domain.com:project-id.mydataset.mytable`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `domain.com:project-id.region-us.INFORMATION_SCHEMA`.JOBS", + "formatted": "SELECT *\nFROM `domain.com:project-id.region-us.INFORMATION_SCHEMA`.JOBS" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `domain.com:project-id.region-us.INFORMATION_SCHEMA.JOBS`", + "formatted": "SELECT *\nFROM `domain.com:project-id.region-us.INFORMATION_SCHEMA.JOBS`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `a.b.com:project-id.mydataset.mytable`", + "formatted": "SELECT *\nFROM `a.b.com:project-id.mydataset.mytable`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `a.b.com:project-id.region-us.INFORMATION_SCHEMA.JOBS`", + "formatted": "SELECT *\nFROM `a.b.com:project-id.region-us.INFORMATION_SCHEMA.JOBS`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM x-0.y", + "formatted": "SELECT *\nFROM x-0.y" + }, + { + "dialect": "bigquery", + "sql": "SELECT `p.d.UdF`(data) FROM `p.d.t`", + "formatted": "SELECT `p.d.UdF`(data)\nFROM `p.d.t`" + }, + { + "dialect": "bigquery", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "bigquery", + "sql": "SELECT ARRAY_CONCAT([1])", + "formatted": "SELECT ARRAY_CONCAT([1])" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM READ_CSV('bla.csv')", + "formatted": "SELECT *\nFROM READ_CSV('bla.csv')" + }, + { + "dialect": "bigquery", + "sql": "SELECT jsondoc['some_key']", + "formatted": "SELECT jsondoc['some_key']" + }, + { + "dialect": "bigquery", + "sql": "SELECT `p.d.UdF`(data).* FROM `p.d.t`", + "formatted": "SELECT `p.d.UdF`(data).*\nFROM `p.d.t`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `my-project.my-dataset.my-table`", + "formatted": "SELECT *\nFROM `my-project.my-dataset.my-table`" + }, + { + "dialect": "bigquery", + "sql": "SELECT x, 1 AS y GROUP BY 1 ORDER BY 1", + "formatted": "SELECT\n x,\n 1 AS y\nGROUP BY\n 1\nORDER BY\n 1" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM x.*", + "formatted": "SELECT *\nFROM x.*" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM x.y*", + "formatted": "SELECT *\nFROM x.y*" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query')", + "formatted": "SELECT SEARCH(data_to_search, 'search_query')" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query', json_scope => 'JSON_KEYS_AND_VALUES')", + "formatted": "SELECT SEARCH(data_to_search, 'search_query', json_scope => 'JSON_KEYS_AND_VALUES')" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query', analyzer => 'PATTERN_ANALYZER')", + "formatted": "SELECT SEARCH(data_to_search, 'search_query', analyzer => 'PATTERN_ANALYZER')" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query', analyzer_options => 'analyzer_options_values')", + "formatted": "SELECT SEARCH(data_to_search, 'search_query', analyzer_options => 'analyzer_options_values')" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query', json_scope => 'JSON_VALUES', analyzer => 'LOG_ANALYZER')", + "formatted": "SELECT\n SEARCH(\n data_to_search,\n 'search_query',\n json_scope => 'JSON_VALUES',\n analyzer => 'LOG_ANALYZER'\n )" + }, + { + "dialect": "bigquery", + "sql": "SELECT SEARCH(data_to_search, 'search_query', analyzer => 'PATTERN_ANALYZER', analyzer_options => 'options')", + "formatted": "SELECT\n SEARCH(\n data_to_search,\n 'search_query',\n analyzer => 'PATTERN_ANALYZER',\n analyzer_options => 'options'\n )" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM dataset.my_table TABLESAMPLE SYSTEM (10 PERCENT)", + "formatted": "SELECT *\nFROM dataset.my_table TABLESAMPLE SYSTEM (10 PERCENT)" + }, + { + "dialect": "bigquery", + "sql": "SELECT '\\n\\r\\a\\v\\f\\t'", + "formatted": "SELECT '\\n\\r\\a\\v\\f\\t'" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM tbl FOR SYSTEM_TIME AS OF z", + "formatted": "SELECT *\nFROM tbl FOR SYSTEM_TIME AS OF z" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM tbl FOR SYSTEM TIME AS OF z", + "formatted": "SELECT *\nFROM tbl FOR SYSTEM TIME AS OF z" + }, + { + "dialect": "bigquery", + "sql": "SELECT PARSE_TIMESTAMP('%c', 'Thu Dec 25 07:30:00 2008', 'UTC')", + "formatted": "SELECT PARSE_TIMESTAMP('%c', 'Thu Dec 25 07:30:00 2008', 'UTC')" + }, + { + "dialect": "bigquery", + "sql": "SELECT ANY_VALUE(fruit HAVING MAX sold) FROM fruits", + "formatted": "SELECT ANY_VALUE(fruit HAVING MAX sold)\nFROM fruits" + }, + { + "dialect": "bigquery", + "sql": "SELECT ANY_VALUE(fruit HAVING MIN sold) FROM fruits", + "formatted": "SELECT ANY_VALUE(fruit HAVING MIN sold)\nFROM fruits" + }, + { + "dialect": "bigquery", + "sql": "SELECT `project-id`.udfs.func(call.dir)", + "formatted": "SELECT `project-id`.udfs.func(call.dir)" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(CURRENT_DATE AS STRING FORMAT 'DAY') AS current_day", + "formatted": "SELECT CAST(CURRENT_DATE AS STRING FORMAT 'DAY') AS current_day" + }, + { + "dialect": "bigquery", + "sql": "SELECT foo IN UNNEST(bar) AS bla", + "formatted": "SELECT foo IN UNNEST(bar) AS bla" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM x-0.a", + "formatted": "SELECT *\nFROM x-0.a" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM pivot CROSS JOIN foo", + "formatted": "SELECT *\nFROM pivot\nCROSS JOIN foo" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM a-b-c.mydataset.mytable", + "formatted": "SELECT *\nFROM a-b-c.mydataset.mytable" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM abc-def-ghi", + "formatted": "SELECT *\nFROM abc-def-ghi" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM a-b-c", + "formatted": "SELECT *\nFROM a-b-c" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM my-table", + "formatted": "SELECT *\nFROM my-table" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM my-project.mydataset.mytable", + "formatted": "SELECT *\nFROM my-project.mydataset.mytable" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM pro-ject_id.c.d CROSS JOIN foo-bar", + "formatted": "SELECT *\nFROM pro-ject_id.c.d\nCROSS JOIN foo-bar" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM foo.bar.25", + "formatted": "SELECT *\nFROM foo.bar.25" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM foo.bar.25_", + "formatted": "SELECT *\nFROM foo.bar.25_" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM foo.bar.25x a", + "formatted": "SELECT *\nFROM foo.bar.25x a" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM foo.bar.25ab c", + "formatted": "SELECT *\nFROM foo.bar.25ab c" + }, + { + "dialect": "bigquery", + "sql": "SELECT b'abc'", + "formatted": "SELECT b'abc'" + }, + { + "dialect": "bigquery", + "sql": "SELECT AS STRUCT 1 AS a, 2 AS b", + "formatted": "SELECT AS STRUCT\n 1 AS a,\n 2 AS b" + }, + { + "dialect": "bigquery", + "sql": "SELECT DISTINCT AS STRUCT 1 AS a, 2 AS b", + "formatted": "SELECT DISTINCT AS STRUCT\n 1 AS a,\n 2 AS b" + }, + { + "dialect": "bigquery", + "sql": "SELECT AS VALUE STRUCT(1 AS a, 2 AS b)", + "formatted": "SELECT AS VALUE STRUCT(1 AS a, 2 AS b)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM q UNPIVOT(values FOR quarter IN (b, c))", + "formatted": "SELECT *\nFROM q UNPIVOT(values FOR quarter IN (b, c))" + }, + { + "dialect": "bigquery", + "sql": "SELECT MAKE_INTERVAL(100, 11, 1, 12, 30, 10)", + "formatted": "SELECT MAKE_INTERVAL(100, 11, 1, 12, 30, 10)" + }, + { + "dialect": "bigquery", + "sql": "SELECT y + 1 FROM x GROUP BY y + 1 ORDER BY 1", + "formatted": "SELECT y + 1\nFROM x\nGROUP BY\n y + 1\nORDER BY\n 1" + }, + { + "dialect": "bigquery", + "sql": "SELECT TIMESTAMP_SECONDS(2) AS t", + "formatted": "SELECT TIMESTAMP_SECONDS(2) AS t" + }, + { + "dialect": "bigquery", + "sql": "SELECT TIMESTAMP_MILLIS(2) AS t", + "formatted": "SELECT TIMESTAMP_MILLIS(2) AS t" + }, + { + "dialect": "bigquery", + "sql": "SELECT COUNT(x RESPECT NULLS)", + "formatted": "SELECT COUNT(x RESPECT NULLS)" + }, + { + "dialect": "bigquery", + "sql": "SELECT LAST_VALUE(x IGNORE NULLS) OVER y AS x", + "formatted": "SELECT LAST_VALUE(x IGNORE NULLS) OVER y AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT ARRAY((SELECT AS STRUCT 1 AS a, 2 AS b))", + "formatted": "SELECT\n ARRAY((\n SELECT AS STRUCT\n 1 AS a,\n 2 AS b\n ))" + }, + { + "dialect": "bigquery", + "sql": "SELECT ARRAY((SELECT AS STRUCT 1 AS a, 2 AS b) LIMIT 10)", + "formatted": "SELECT\n ARRAY((\n SELECT AS STRUCT\n 1 AS a,\n 2 AS b\n ) LIMIT 10)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM x WHERE x.y >= (SELECT MAX(a) FROM b-c) - 20", + "formatted": "SELECT *\nFROM x\nWHERE\n x.y >= (\n SELECT MAX(a)\n FROM b-c\n )\n - 20" + }, + { + "dialect": "bigquery", + "sql": "SELECT FORMAT_TIMESTAMP('%F %T', CURRENT_TIMESTAMP(), 'Europe/Berlin') AS ts", + "formatted": "SELECT FORMAT_TIMESTAMP('%F %T', CURRENT_TIMESTAMP(), 'Europe/Berlin') AS ts" + }, + { + "dialect": "bigquery", + "sql": "SELECT cars, apples FROM some_table PIVOT(SUM(total_counts) FOR products IN ('general.cars' AS cars, 'food.apples' AS apples))", + "formatted": "SELECT\n cars,\n apples\nFROM some_table PIVOT(SUM(total_counts) FOR products IN ('general.cars' AS cars, 'food.apples' AS apples))" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM test QUALIFY a IS DISTINCT FROM b WINDOW c AS (PARTITION BY d)", + "formatted": "SELECT *\nFROM test\nQUALIFY a IS DISTINCT FROM b\nWINDOW\n c AS (PARTITION BY d)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM (SELECT * FROM `t`) AS a UNPIVOT((c) FOR c_name IN (v1, v2))", + "formatted": "SELECT *\nFROM (\n SELECT *\n FROM `t`\n) AS a UNPIVOT((c) FOR c_name IN (v1, v2))" + }, + { + "dialect": "bigquery", + "sql": "SELECT ROW() OVER (y ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) FROM x WINDOW y AS (PARTITION BY CATEGORY)", + "formatted": "SELECT ROW() OVER (y ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING)\nFROM x\nWINDOW\n y AS (PARTITION BY CATEGORY)" + }, + { + "dialect": "bigquery", + "sql": "SELECT item, purchases, LAST_VALUE(item) OVER (item_window ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) AS most_popular FROM Produce WINDOW item_window AS (ORDER BY purchases)", + "formatted": "SELECT\n item,\n purchases,\n LAST_VALUE(item) OVER (item_window ROWS BETWEEN 2 PRECEDING AND 2 FOLLOWING) AS most_popular\nFROM Produce\nWINDOW\n item_window AS (ORDER BY purchases)" + }, + { + "dialect": "bigquery", + "sql": "SELECT LAST_VALUE(a IGNORE NULLS) OVER y FROM x WINDOW y AS (PARTITION BY CATEGORY)", + "formatted": "SELECT LAST_VALUE(a IGNORE NULLS) OVER y\nFROM x\nWINDOW\n y AS (PARTITION BY CATEGORY)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM foo AS t0 FOR SYSTEM_TIME AS OF '2026-02-12T23:22:21.743416+00:00'", + "formatted": "SELECT *\nFROM foo AS t0 FOR SYSTEM_TIME AS OF '2026-02-12T23:22:21.743416+00:00'" + }, + { + "dialect": "bigquery", + "sql": "SELECT b\"\\x0a$'x'00\"", + "formatted": "SELECT b\"\\x0a$'x'00\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM t1, t2", + "formatted": "SELECT *\nFROM t1, t2" + }, + { + "dialect": "bigquery", + "sql": "SELECT r\"\\t\"", + "formatted": "SELECT r\"\\t\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM `proj.dataset.INFORMATION_SCHEMA.SOME_VIEW`", + "formatted": "SELECT *\nFROM `proj.dataset.INFORMATION_SCHEMA.SOME_VIEW`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM region_or_dataset.INFORMATION_SCHEMA.TABLES", + "formatted": "SELECT *\nFROM region_or_dataset.INFORMATION_SCHEMA.TABLES" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM region_or_dataset.INFORMATION_SCHEMA.TABLES AS some_name", + "formatted": "SELECT *\nFROM region_or_dataset.INFORMATION_SCHEMA.TABLES AS some_name" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM proj.region_or_dataset.INFORMATION_SCHEMA.TABLES", + "formatted": "SELECT *\nFROM proj.region_or_dataset.INFORMATION_SCHEMA.TABLES" + }, + { + "dialect": "bigquery", + "sql": "SELECT ARRAY(SELECT AS STRUCT 1 a, 2 b)", + "formatted": "SELECT\n ARRAY(SELECT AS STRUCT\n 1 a,\n 2 b)", + "previously_formatted": "SELECT\n ARRAY(SELECT AS STRUCT\n 1 AS a,\n 2 AS b)" + }, + { + "dialect": "bigquery", + "sql": "select array_contains([1, 2, 3], 1)", + "formatted": "SELECT array_contains([1, 2, 3], 1)" + }, + { + "dialect": "bigquery", + "sql": "SELECT SPLIT(foo)", + "formatted": "SELECT SPLIT(foo)" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS hash", + "formatted": "SELECT 1 AS hash" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS at", + "formatted": "SELECT 1 AS at" + }, + { + "dialect": "bigquery", + "sql": "SELECT \"\"\"ends with \\\"word\\\"\"\"\"", + "formatted": "SELECT \"\"\"ends with \\\"word\\\"\"\"\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT '''ends with \\'word\\''''", + "formatted": "SELECT '''ends with \\'word\\''''" + }, + { + "dialect": "bigquery", + "sql": "SELECT \"\"\"a\\\"b\"\"\"", + "formatted": "SELECT \"\"\"a\\\"b\"\"\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT r\"\"\"ends with \\\"\"\"\"", + "formatted": "SELECT r\"\"\"ends with \\\"\"\"\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT r\"\"\"a\\\"b\"\"\"", + "formatted": "SELECT r\"\"\"a\\\"b\"\"\"" + }, + { + "dialect": "bigquery", + "sql": "SELECT a overlaps", + "formatted": "SELECT a overlaps", + "previously_formatted": "SELECT\n a AS overlaps" + }, + { + "dialect": "bigquery", + "sql": "SELECT y + 1 z FROM x GROUP BY y + 1 ORDER BY z", + "formatted": "SELECT y + 1 z\nFROM x\nGROUP BY\n y + 1\nORDER BY\n z", + "previously_formatted": "SELECT\n y + 1 AS z\nFROM x\nGROUP BY\n y + 1\nORDER BY\n z" + }, + { + "dialect": "bigquery", + "sql": "SELECT y + 1 z FROM x GROUP BY y + 1", + "formatted": "SELECT y + 1 z\nFROM x\nGROUP BY\n y + 1", + "previously_formatted": "SELECT\n y + 1 AS z\nFROM x\nGROUP BY\n y + 1" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON '\"foo\"' AS json_data", + "formatted": "SELECT JSON '\"foo\"' AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM (SELECT a, b, c FROM test) PIVOT(SUM(b) d, COUNT(*) e FOR c IN ('x', 'y'))", + "formatted": "SELECT *\nFROM (\n SELECT\n a,\n b,\n c\n FROM test\n) PIVOT(SUM(b) d, COUNT(*) e FOR c IN ('x', 'y'))", + "previously_formatted": "SELECT\n *\nFROM (\n SELECT\n a,\n b,\n c\n FROM test\n) PIVOT(SUM(b) AS d, COUNT(*) AS e FOR c IN ('x', 'y'))" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(1 AS BYTEINT)", + "formatted": "SELECT CAST(1 AS BYTEINT)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM a-b c", + "formatted": "SELECT *\nFROM a-b c", + "previously_formatted": "SELECT\n *\nFROM a-b AS c" + }, + { + "dialect": "bigquery", + "sql": "SELECT PARSE_DATETIME('%a %b %e %I:%M:%S %Y', 'Thu Dec 25 07:30:00 2008')", + "formatted": "SELECT PARSE_DATETIME('%a %b %e %I:%M:%S %Y', 'Thu Dec 25 07:30:00 2008')" + }, + { + "dialect": "bigquery", + "sql": "SELECT TRANSLATE(MODEL, 'in', 't') FROM (SELECT 'input' AS MODEL)", + "formatted": "SELECT TRANSLATE(MODEL, 'in', 't')\nFROM (\n SELECT 'input' AS MODEL\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT GRANT FROM (SELECT 'input' AS GRANT)", + "formatted": "SELECT GRANT\nFROM (\n SELECT 'input' AS GRANT\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(1 AS NUMERIC(10, 2))", + "formatted": "SELECT CAST(1 AS NUMERIC(10, 2))" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST('1' AS STRING(10)) UNION ALL SELECT CAST('2' AS STRING(10))", + "formatted": "SELECT CAST('1' AS STRING(10))\nUNION ALL\nSELECT CAST('2' AS STRING(10))" + }, + { + "dialect": "bigquery", + "sql": "SELECT cola FROM (SELECT CAST('1' AS STRING(10)) AS cola UNION ALL SELECT CAST('2' AS STRING(10)) AS cola)", + "formatted": "SELECT cola\nFROM (\n SELECT CAST('1' AS STRING(10)) AS cola\n UNION ALL\n SELECT CAST('2' AS STRING(10)) AS cola\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM GAP_FILL(TABLE device_data, ts_column => 'time', bucket_width => INTERVAL '1' MINUTE, value_columns => [('signal', 'locf')]) ORDER BY time", + "formatted": "SELECT *\nFROM GAP_FILL(\n TABLE device_data,\n ts_column => 'time',\n bucket_width => INTERVAL '1' MINUTE,\n value_columns => [('signal', 'locf')]\n)\nORDER BY\n time" + }, + { + "dialect": "bigquery", + "sql": "SELECT a, b, c, d, e FROM GAP_FILL(TABLE foo, ts_column => 'b', partitioning_columns => ['a'], value_columns => [('c', 'bar'), ('d', 'baz'), ('e', 'bla')], bucket_width => INTERVAL '1' DAY)", + "formatted": "SELECT\n a,\n b,\n c,\n d,\n e\nFROM GAP_FILL(\n TABLE foo,\n ts_column => 'b',\n partitioning_columns => ['a'],\n value_columns => [\n ('c', 'bar'),\n ('d', 'baz'),\n ('e', 'bla')\n ],\n bucket_width => INTERVAL '1' DAY\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM GAP_FILL(TABLE device_data, ts_column => 'time', bucket_width => INTERVAL '1' MINUTE, value_columns => [('signal', 'linear')], ignore_null_values => FALSE) ORDER BY time", + "formatted": "SELECT *\nFROM GAP_FILL(\n TABLE device_data,\n ts_column => 'time',\n bucket_width => INTERVAL '1' MINUTE,\n value_columns => [('signal', 'linear')],\n ignore_null_values => FALSE\n)\nORDER BY\n time" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM GAP_FILL(TABLE device_data, ts_column => 'time', bucket_width => INTERVAL '1' MINUTE) ORDER BY time", + "formatted": "SELECT *\nFROM GAP_FILL(\n TABLE device_data,\n ts_column => 'time',\n bucket_width => INTERVAL '1' MINUTE\n)\nORDER BY\n time" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM GAP_FILL(TABLE device_data, ts_column => 'time', bucket_width => INTERVAL '1' MINUTE, value_columns => [('signal', 'null')], origin => CAST('2023-11-01 09:30:01' AS DATETIME)) ORDER BY time", + "formatted": "SELECT *\nFROM GAP_FILL(\n TABLE device_data,\n ts_column => 'time',\n bucket_width => INTERVAL '1' MINUTE,\n value_columns => [('signal', 'null')],\n origin => CAST('2023-11-01 09:30:01' AS DATETIME)\n)\nORDER BY\n time" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM GAP_FILL(TABLE device_data, ts_column => 'time', bucket_width => INTERVAL '1' MINUTE, value_columns => [('signal', 'locf')]) ORDER BY time", + "formatted": "SELECT *\nFROM GAP_FILL(\n TABLE device_data,\n ts_column => 'time',\n bucket_width => INTERVAL '1' MINUTE,\n value_columns => [('signal', 'locf')]\n)\nORDER BY\n time" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.PREDICT(MODEL mydataset.mymodel, (SELECT label, column1, column2 FROM mydataset.mytable))", + "formatted": "SELECT *\nFROM ML.PREDICT(MODEL mydataset.mymodel, (\n SELECT\n label,\n column1,\n column2\n FROM mydataset.mytable\n))" + }, + { + "dialect": "bigquery", + "sql": "SELECT label, predicted_label1, predicted_label AS predicted_label2 FROM ML.PREDICT(MODEL mydataset.mymodel2, (SELECT * EXCEPT (predicted_label), predicted_label AS predicted_label1 FROM ML.PREDICT(MODEL mydataset.mymodel1, TABLE mydataset.mytable)))", + "formatted": "SELECT\n label,\n predicted_label1,\n predicted_label AS predicted_label2\nFROM ML.PREDICT(\n MODEL mydataset.mymodel2,\n (\n SELECT\n * EXCEPT (predicted_label),\n predicted_label AS predicted_label1\n FROM ML.PREDICT(MODEL mydataset.mymodel1, TABLE mydataset.mytable)\n )\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.PREDICT(MODEL mydataset.mymodel, (SELECT custom_label, column1, column2 FROM mydataset.mytable), STRUCT(0.55 AS threshold))", + "formatted": "SELECT *\nFROM ML.PREDICT(\n MODEL mydataset.mymodel,\n (\n SELECT\n custom_label,\n column1,\n column2\n FROM mydataset.mytable\n ),\n STRUCT(0.55 AS threshold)\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT COSH(1.5)", + "formatted": "SELECT COSH(1.5)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.PREDICT(MODEL `my_project`.my_dataset.my_model, (SELECT * FROM input_data))", + "formatted": "SELECT *\nFROM ML.PREDICT(MODEL `my_project`.my_dataset.my_model, (\n SELECT *\n FROM input_data\n))" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.PREDICT(MODEL my_dataset.vision_model, (SELECT uri, ML.RESIZE_IMAGE(ML.DECODE_IMAGE(data), 480, 480, FALSE) AS input FROM my_dataset.object_table))", + "formatted": "SELECT *\nFROM ML.PREDICT(\n MODEL my_dataset.vision_model,\n (\n SELECT\n uri,\n ML.RESIZE_IMAGE(ML.DECODE_IMAGE(data), 480, 480, FALSE) AS input\n FROM my_dataset.object_table\n )\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.PREDICT(MODEL my_dataset.vision_model, (SELECT uri, ML.CONVERT_COLOR_SPACE(ML.RESIZE_IMAGE(ML.DECODE_IMAGE(data), 224, 280, TRUE), 'YIQ') AS input FROM my_dataset.object_table WHERE content_type = 'image/jpeg'))", + "formatted": "SELECT *\nFROM ML.PREDICT(\n MODEL my_dataset.vision_model,\n (\n SELECT\n uri,\n ML.CONVERT_COLOR_SPACE(ML.RESIZE_IMAGE(ML.DECODE_IMAGE(data), 224, 280, TRUE), 'YIQ') AS input\n FROM my_dataset.object_table\n WHERE content_type = 'image/jpeg'\n )\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.FEATURES_AT_TIME((SELECT 1), num_rows => 1)", + "formatted": "SELECT *\nFROM ML.FEATURES_AT_TIME((\n SELECT 1\n), num_rows => 1)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.FEATURES_AT_TIME(TABLE mydataset.feature_table, time => '2022-06-11 10:00:00+00', num_rows => 1, ignore_feature_nulls => TRUE)", + "formatted": "SELECT *\nFROM ML.FEATURES_AT_TIME(\n TABLE mydataset.feature_table,\n time => '2022-06-11 10:00:00+00',\n num_rows => 1,\n ignore_feature_nulls => TRUE\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM VECTOR_SEARCH(TABLE mydataset.base_table, 'column_to_search', TABLE mydataset.query_table, 'query_column_to_search', top_k => 2, distance_type => 'cosine', options => '{\"fraction_lists_to_search\":0.15}')", + "formatted": "SELECT *\nFROM VECTOR_SEARCH(\n TABLE mydataset.base_table,\n 'column_to_search',\n TABLE mydataset.query_table,\n 'query_column_to_search',\n top_k => 2,\n distance_type => 'cosine',\n options => '{\"fraction_lists_to_search\":0.15}'\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM VECTOR_SEARCH(TABLE mydataset.base_table, 'column_to_search', TABLE mydataset.query_table, query_column_to_search => 'query_column_to_search', top_k => 2, distance_type => 'cosine', options => '{\"fraction_lists_to_search\":0.15}')", + "formatted": "SELECT *\nFROM VECTOR_SEARCH(\n TABLE mydataset.base_table,\n 'column_to_search',\n TABLE mydataset.query_table,\n query_column_to_search => 'query_column_to_search',\n top_k => 2,\n distance_type => 'cosine',\n options => '{\"fraction_lists_to_search\":0.15}'\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM VECTOR_SEARCH((SELECT * FROM mydataset.base_table), 'column_to_search', (SELECT * FROM mydataset.query_table), 'query_column_to_search')", + "formatted": "SELECT *\nFROM VECTOR_SEARCH(\n (\n SELECT *\n FROM mydataset.base_table\n ),\n 'column_to_search',\n (\n SELECT *\n FROM mydataset.query_table\n ),\n 'query_column_to_search'\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM VECTOR_SEARCH(TABLE mydataset.base_table, 'column_to_search', TABLE mydataset.query_table)", + "formatted": "SELECT *\nFROM VECTOR_SEARCH(TABLE mydataset.base_table, 'column_to_search', TABLE mydataset.query_table)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.TRANSLATE(MODEL `mydataset.mytranslatemodel`, TABLE `mydataset.mybqtable`, STRUCT('translate_text' AS translate_mode, 'zh-CN' AS target_language_code))", + "formatted": "SELECT *\nFROM ML.TRANSLATE(\n MODEL `mydataset.mytranslatemodel`,\n TABLE `mydataset.mybqtable`,\n STRUCT(\n 'translate_text' AS translate_mode,\n 'zh-CN' AS target_language_code\n )\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.FORECAST(MODEL `mydataset.mymodel`, STRUCT(2 AS horizon))", + "formatted": "SELECT *\nFROM ML.FORECAST(MODEL `mydataset.mymodel`, STRUCT(2 AS horizon))" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.FORECAST(MODEL `mydataset.mymodel`, TABLE `mydataset.mybqtable`, STRUCT(2 AS horizon, 4 AS confidence_level))", + "formatted": "SELECT *\nFROM ML.FORECAST(\n MODEL `mydataset.mymodel`,\n TABLE `mydataset.mybqtable`,\n STRUCT(2 AS horizon, 4 AS confidence_level)\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.FORECAST(MODEL `mydataset.mymodel`, (SELECT * FROM mydataset.query_table), STRUCT())", + "formatted": "SELECT *\nFROM ML.FORECAST(MODEL `mydataset.mymodel`, (\n SELECT *\n FROM mydataset.query_table\n), STRUCT())" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM AI.FORECAST(TABLE citibike_trips, data_col => 'num_trips', timestamp_col => 'date', horizon => 30)", + "formatted": "SELECT *\nFROM AI.FORECAST(\n TABLE citibike_trips,\n data_col => 'num_trips',\n timestamp_col => 'date',\n horizon => 30\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM AI.FORECAST((SELECT * FROM citibike_trips), data_col => 'num_trips', timestamp_col => 'date', horizon => 30)", + "formatted": "SELECT *\nFROM AI.FORECAST(\n (\n SELECT *\n FROM citibike_trips\n ),\n data_col => 'num_trips',\n timestamp_col => 'date',\n horizon => 30\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.GENERATE_TEXT(MODEL `mydataset.gemini_model`, TABLE `mydataset.prompt_table`, STRUCT(0.15 AS temperature))", + "formatted": "SELECT *\nFROM ML.GENERATE_TEXT(\n MODEL `mydataset.gemini_model`,\n TABLE `mydataset.prompt_table`,\n STRUCT(0.15 AS temperature)\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM AI.GENERATE_TEXT(MODEL `mydataset.gemini_model`, TABLE `mydataset.prompt_table`, STRUCT(0.15 AS temperature))", + "formatted": "SELECT *\nFROM AI.GENERATE_TEXT(\n MODEL `mydataset.gemini_model`,\n TABLE `mydataset.prompt_table`,\n STRUCT(0.15 AS temperature)\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM AI.GENERATE_TABLE(MODEL `mydataset.gemini_model`, (SELECT 'Q' AS prompt), STRUCT('name STRING' AS output_schema))", + "formatted": "SELECT *\nFROM AI.GENERATE_TABLE(\n MODEL `mydataset.gemini_model`,\n (\n SELECT 'Q' AS prompt\n ),\n STRUCT('name STRING' AS output_schema)\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT AI.GENERATE_BOOL(MODEL `mydataset.gemini_model`, 'Is sky blue?')", + "formatted": "SELECT AI.GENERATE_BOOL(MODEL `mydataset.gemini_model`, 'Is sky blue?')" + }, + { + "dialect": "bigquery", + "sql": "SELECT AI.EMBED('hello')", + "formatted": "SELECT AI.EMBED('hello')" + }, + { + "dialect": "bigquery", + "sql": "SELECT AI.SIMILARITY('a', 'b')", + "formatted": "SELECT AI.SIMILARITY('a', 'b')" + }, + { + "dialect": "bigquery", + "sql": "SELECT AI.GENERATE('Write a haiku')", + "formatted": "SELECT AI.GENERATE('Write a haiku')" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT() AS json_data", + "formatted": "SELECT JSON_OBJECT() AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT('foo', 10, 'bar', TRUE) AS json_data", + "formatted": "SELECT JSON_OBJECT('foo', 10, 'bar', TRUE) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT('foo', 10, 'bar', ['a', 'b']) AS json_data", + "formatted": "SELECT JSON_OBJECT('foo', 10, 'bar', ['a', 'b']) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT('a', 10, 'a', 'foo') AS json_data", + "formatted": "SELECT JSON_OBJECT('a', 10, 'a', 'foo') AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT(['a', 'b'], [10, NULL]) AS json_data", + "formatted": "SELECT JSON_OBJECT(['a', 'b'], [10, NULL]) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT(['a', 'b'], [JSON '10', JSON '\"foo\"']) AS json_data", + "formatted": "SELECT JSON_OBJECT(['a', 'b'], [JSON '10', JSON '\"foo\"']) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT(['a', 'b'], [STRUCT(10 AS id, 'Red' AS color), STRUCT(20 AS id, 'Blue' AS color)]) AS json_data", + "formatted": "SELECT\n JSON_OBJECT(['a', 'b'], [\n STRUCT(10 AS id, 'Red' AS color),\n STRUCT(20 AS id, 'Blue' AS color)\n ]) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT JSON_OBJECT(['a', 'b'], [TO_JSON(10), TO_JSON(['foo', 'bar'])]) AS json_data", + "formatted": "SELECT JSON_OBJECT(['a', 'b'], [TO_JSON(10), TO_JSON(['foo', 'bar'])]) AS json_data" + }, + { + "dialect": "bigquery", + "sql": "SELECT MOD((SELECT 1), 2)", + "formatted": "SELECT\n MOD((\n SELECT 1\n ), 2)" + }, + { + "dialect": "bigquery", + "sql": "SELECT STRUCT>([\"2023-01-17\"])", + "formatted": "SELECT STRUCT> ([\"2023-01-17\"])" + }, + { + "dialect": "bigquery", + "sql": "SELECT STRUCT((SELECT 'foo')).*", + "formatted": "SELECT\n STRUCT ((\n SELECT 'foo'\n )) .*" + }, + { + "dialect": "bigquery", + "sql": "SELECT RANGE(CAST('2022-12-01' AS DATE), CAST('2022-12-31' AS DATE))", + "formatted": "SELECT\n RANGE(\n CAST('2022-12-01' AS DATE),\n CAST('2022-12-31' AS DATE)\n )" + }, + { + "dialect": "bigquery", + "sql": "SELECT RANGE(NULL, CAST('2022-12-31' AS DATE))", + "formatted": "SELECT RANGE(NULL, CAST('2022-12-31' AS DATE))" + }, + { + "dialect": "bigquery", + "sql": "SELECT RANGE(CAST('2022-10-01 14:53:27' AS DATETIME), CAST('2022-10-01 16:00:00' AS DATETIME))", + "formatted": "SELECT\n RANGE(\n CAST('2022-10-01 14:53:27' AS DATETIME),\n CAST('2022-10-01 16:00:00' AS DATETIME)\n )" + }, + { + "dialect": "bigquery", + "sql": "SELECT RANGE(CAST('2022-10-01 14:53:27 America/Los_Angeles' AS TIMESTAMP), CAST('2022-10-01 16:00:00 America/Los_Angeles' AS TIMESTAMP))", + "formatted": "SELECT\n RANGE(\n CAST('2022-10-01 14:53:27 America/Los_Angeles' AS TIMESTAMP),\n CAST('2022-10-01 16:00:00 America/Los_Angeles' AS TIMESTAMP)\n )" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST('2026-03-24' AS STRING FORMAT ('YYYY'))", + "formatted": "SELECT CAST('2026-03-24' AS STRING FORMAT('YYYY'))" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(date AS STRING FORMAT ('YYYY')) FROM (SELECT DATE('2026-03-24') AS date)", + "formatted": "SELECT CAST(date AS STRING FORMAT('YYYY'))\nFROM (\n SELECT DATE('2026-03-24') AS date\n)" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(date AS STRING FORMAT ('YYYY-MM-DD'))", + "formatted": "SELECT CAST(date AS STRING FORMAT('YYYY-MM-DD'))" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(timestamp AS STRING FORMAT ('YYYY-MM-DD') AT TIME ZONE 'UTC')", + "formatted": "SELECT CAST(timestamp AS STRING FORMAT('YYYY-MM-DD') AT TIME ZONE 'UTC')" + }, + { + "dialect": "bigquery", + "sql": "SELECT CAST(date AS TIMESTAMP FORMAT ('YYYY-MM-DD HH24:MI:SS'))", + "formatted": "SELECT CAST(date AS TIMESTAMP FORMAT('YYYY-MM-DD HH24:MI:SS'))" + }, + { + "dialect": "bigquery", + "sql": "SELECT a, GROUP_CONCAT(b) FROM table GROUP BY a", + "formatted": "SELECT\n a,\n GROUP_CONCAT(b)\nFROM table\nGROUP BY\n a" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS foo INNER UNION ALL SELECT 3 AS foo, 4 AS bar", + "formatted": "SELECT 1 AS foo\nINNER UNION ALL\nSELECT\n 3 AS foo,\n 4 AS bar" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS x UNION ALL CORRESPONDING SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nUNION ALL CORRESPONDING\nSELECT 2 AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS x UNION ALL CORRESPONDING BY (foo, bar) SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nUNION ALL CORRESPONDING BY (foo, bar)\nSELECT 2 AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS x LEFT UNION ALL CORRESPONDING SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nLEFT UNION ALL CORRESPONDING\nSELECT 2 AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS x UNION ALL STRICT CORRESPONDING SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nUNION ALL STRICT CORRESPONDING\nSELECT 2 AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT 1 AS x UNION ALL STRICT CORRESPONDING BY (foo, bar) SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nUNION ALL STRICT CORRESPONDING BY (foo, bar)\nSELECT 2 AS x" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM UNNEST(x) WITH OFFSET EXCEPT DISTINCT SELECT * FROM UNNEST(y) WITH OFFSET", + "formatted": "SELECT *\nFROM UNNEST(x) WITH OFFSET\nEXCEPT DISTINCT\nSELECT *\nFROM UNNEST(y) WITH OFFSET" + }, + { + "dialect": "bigquery", + "sql": "SELECT col FROM t WHERE _PARTITIONTIME BETWEEN a AND b", + "formatted": "SELECT col\nFROM t\nWHERE _PARTITIONTIME BETWEEN a AND b" + }, + { + "dialect": "bigquery", + "sql": "SELECT _DBT_MAX_PARTITION FROM t", + "formatted": "SELECT _DBT_MAX_PARTITION\nFROM t" + }, + { + "dialect": "bigquery", + "sql": "WITH cte(c) AS (SELECT * FROM t) SELECT * FROM cte", + "formatted": "WITH cte(c) AS (\n SELECT *\n FROM t\n)\n\nSELECT *\nFROM cte" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM t AS t(c1, c2)", + "formatted": "SELECT *\nFROM t AS t(c1, c2)" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM t AS t(c1, c2)", + "formatted": "SELECT *\nFROM t AS t(c1, c2)" + }, + { + "dialect": "bigquery", + "sql": "WITH foo AS (SELECT [1, 2, 3] AS array_col) SELECT array_col[offset] FROM foo CROSS JOIN UNNEST(array_col) WITH OFFSET AS offset", + "formatted": "WITH foo AS (\n SELECT [1, 2, 3] AS array_col\n)\n\nSELECT array_col[offset]\nFROM foo\nCROSS JOIN UNNEST(array_col) WITH OFFSET AS offset" + }, + { + "dialect": "bigquery", + "sql": "SELECT `db.t`.`c` FROM `db.t`", + "formatted": "SELECT `db.t`.`c`\nFROM `db.t`" + }, + { + "dialect": "bigquery", + "sql": "SELECT `p.d.t`.`c`.`f` FROM `p.d.t`", + "formatted": "SELECT `p.d.t`.`c`.`f`\nFROM `p.d.t`" + }, + { + "dialect": "bigquery", + "sql": "SELECT * FROM ML.TRANSLATE(MODEL `mydataset.mymodel`, (SELECT comment AS text_content FROM mydataset.mytable), STRUCT('translate_text' AS translate_mode, 'en' AS target_language_code))", + "formatted": "SELECT *\nFROM ML.TRANSLATE(\n MODEL `mydataset.mymodel`,\n (\n SELECT comment AS text_content\n FROM mydataset.mytable\n ),\n STRUCT(\n 'translate_text' AS translate_mode,\n 'en' AS target_language_code\n )\n)" + }, + { + "dialect": "bigquery", + "sql": "WITH t AS (SELECT '{\"x-y\": \"z\"}' AS c) SELECT JSON_EXTRACT(c, '$.x-y') FROM t", + "formatted": "WITH t AS (\n SELECT '{\"x-y\": \"z\"}' AS c\n)\n\nSELECT JSON_EXTRACT(c, '$.x-y')\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('a' AS STRING COLLATE UTF8_BINARY)", + "formatted": "SELECT CAST('a' AS STRING COLLATE UTF8_BINARY)" + }, + { + "dialect": "databricks", + "sql": "SELECT COSH(1.5)", + "formatted": "SELECT COSH(1.5)" + }, + { + "dialect": "databricks", + "sql": "SELECT BITMAP_BIT_POSITION(10)", + "formatted": "SELECT BITMAP_BIT_POSITION(10)" + }, + { + "dialect": "databricks", + "sql": "SELECT BITMAP_BUCKET_NUMBER(32769)", + "formatted": "SELECT BITMAP_BUCKET_NUMBER(32769)" + }, + { + "dialect": "databricks", + "sql": "SELECT BITMAP_CONSTRUCT_AGG(value)", + "formatted": "SELECT BITMAP_CONSTRUCT_AGG(value)" + }, + { + "dialect": "databricks", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "databricks", + "sql": "SELECT MODE(category)", + "formatted": "SELECT MODE(category)" + }, + { + "dialect": "databricks", + "sql": "SELECT MODE(price, TRUE) AS deterministic_mode FROM products", + "formatted": "SELECT MODE(price, TRUE) AS deterministic_mode\nFROM products" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST(NULL AS VOID)", + "formatted": "SELECT CAST(NULL AS VOID)" + }, + { + "dialect": "databricks", + "sql": "SELECT void FROM t", + "formatted": "SELECT void\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT * FROM stream", + "formatted": "SELECT *\nFROM stream" + }, + { + "dialect": "databricks", + "sql": "SELECT * FROM STREAM t", + "formatted": "SELECT *\nFROM STREAM t" + }, + { + "dialect": "databricks", + "sql": "SELECT t.current_time FROM t", + "formatted": "SELECT t.current_time\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT col1 FROM t CLUSTER BY col1", + "formatted": "SELECT col1\nFROM t\nCLUSTER BY\n col1" + }, + { + "dialect": "databricks", + "sql": "SELECT col1, col2 FROM t CLUSTER BY col1, col2", + "formatted": "SELECT\n col1,\n col2\nFROM t\nCLUSTER BY\n col1,\n col2" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO HOUR)", + "formatted": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO HOUR)" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO MINUTE)", + "formatted": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO MINUTE)" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO SECOND)", + "formatted": "SELECT CAST('11 23:4:0' AS INTERVAL DAY TO SECOND)" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('23:00:00' AS INTERVAL HOUR TO MINUTE)", + "formatted": "SELECT CAST('23:00:00' AS INTERVAL HOUR TO MINUTE)" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('23:00:00' AS INTERVAL HOUR TO SECOND)", + "formatted": "SELECT CAST('23:00:00' AS INTERVAL HOUR TO SECOND)" + }, + { + "dialect": "databricks", + "sql": "SELECT CAST('23:00:00' AS INTERVAL MINUTE TO SECOND)", + "formatted": "SELECT CAST('23:00:00' AS INTERVAL MINUTE TO SECOND)" + }, + { + "dialect": "databricks", + "sql": "SELECT ${x} FROM ${y} WHERE ${z} > 1", + "formatted": "SELECT ${x}\nFROM ${y}\nWHERE ${z} > 1" + }, + { + "dialect": "databricks", + "sql": "SELECT PARSE_JSON('{}')", + "formatted": "SELECT PARSE_JSON('{}')" + }, + { + "dialect": "databricks", + "sql": "SELECT RANDSTR(123)", + "formatted": "SELECT RANDSTR(123)" + }, + { + "dialect": "databricks", + "sql": "SELECT RANDSTR(123, 456)", + "formatted": "SELECT RANDSTR(123, 456)" + }, + { + "dialect": "databricks", + "sql": "SELECT * FROM sales UNPIVOT INCLUDE NULLS (sales FOR quarter IN (q1 AS `Jan-Mar`))", + "formatted": "SELECT *\nFROM sales UNPIVOT INCLUDE NULLS (sales FOR quarter IN (q1 AS `Jan-Mar`))" + }, + { + "dialect": "databricks", + "sql": "SELECT * FROM sales UNPIVOT EXCLUDE NULLS (sales FOR quarter IN (q1 AS `Jan-Mar`))", + "formatted": "SELECT *\nFROM sales UNPIVOT EXCLUDE NULLS (sales FOR quarter IN (q1 AS `Jan-Mar`))" + }, + { + "dialect": "databricks", + "sql": "SELECT * FROM t1, t2", + "formatted": "SELECT *\nFROM t1, t2" + }, + { + "dialect": "databricks", + "sql": "SELECT TIMESTAMP '2025-04-29 18.47.18'::DATE", + "formatted": "SELECT TIMESTAMP '2025-04-29 18.47.18'::DATE" + }, + { + "dialect": "databricks", + "sql": "SELECT DATE_FORMAT(CAST(FROM_UTC_TIMESTAMP(foo, 'America/Los_Angeles') AS TIMESTAMP), 'yyyy-MM-dd HH:mm:ss') AS foo FROM t", + "formatted": "SELECT\n DATE_FORMAT(\n CAST(FROM_UTC_TIMESTAMP(foo, 'America/Los_Angeles') AS TIMESTAMP),\n 'yyyy-MM-dd HH:mm:ss'\n ) AS foo\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT r\"\\\\foo.bar\\\"", + "formatted": "SELECT r\"\\\\foo.bar\\\"" + }, + { + "dialect": "databricks", + "sql": "SELECT SUBSTRING_INDEX(str, delim, count)", + "formatted": "SELECT SUBSTRING_INDEX(str, delim, count)" + }, + { + "dialect": "databricks", + "sql": "SELECT SUBSTR('Spark' FROM 5 FOR 1)", + "formatted": "SELECT SUBSTR('Spark' FROM 5 FOR 1)" + }, + { + "dialect": "databricks", + "sql": "SELECT SUBSTR('Spark SQL', 5)", + "formatted": "SELECT SUBSTR('Spark SQL', 5)" + }, + { + "dialect": "databricks", + "sql": "SELECT SUBSTR(ENCODE('Spark SQL', 'utf-8'), 5)", + "formatted": "SELECT SUBSTR(ENCODE('Spark SQL', 'utf-8'), 5)" + }, + { + "dialect": "databricks", + "sql": "SELECT test, LISTAGG(email, '') AS Email FROM organizations GROUP BY test", + "formatted": "SELECT\n test,\n LISTAGG(email, '') AS Email\nFROM organizations\nGROUP BY\n test" + }, + { + "dialect": "databricks", + "sql": "WITH t AS (VALUES ('foo_val') AS t(foo1)) SELECT foo1 FROM t", + "formatted": "WITH t AS (\n VALUES ('foo_val') AS t(foo1)\n)\n\nSELECT foo1\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT ELT(2, 'foo', 'bar', 'baz') AS Result", + "formatted": "SELECT ELT(2, 'foo', 'bar', 'baz') AS Result" + }, + { + "dialect": "databricks", + "sql": "SELECT MAKE_INTERVAL(100, 11, 12, 13, 14, 14, 15)", + "formatted": "SELECT MAKE_INTERVAL(100, 11, 12, 13, 14, 14, 15)" + }, + { + "dialect": "databricks", + "sql": "SELECT name, GROUPING_ID() FROM customer GROUP BY ROLLUP (name)", + "formatted": "SELECT\n name,\n GROUPING_ID()\nFROM customer\nGROUP BY\n ROLLUP (\n name\n )" + }, + { + "dialect": "databricks", + "sql": "SELECT CURDATE()", + "formatted": "SELECT CURDATE()" + }, + { + "dialect": "databricks", + "sql": "SELECT c1:price, c1:price.foo, c1:price.bar[1]", + "formatted": "SELECT\n c1:price,\n c1:price.foo,\n c1:price.bar[1]" + }, + { + "dialect": "databricks", + "sql": "SELECT TRY_CAST(c1:price AS ARRAY)", + "formatted": "SELECT TRY_CAST(c1:price AS ARRAY)" + }, + { + "dialect": "databricks", + "sql": "SELECT TRY_CAST(c1:[\"foo bar\"][\"baz qux\"] AS ARRAY)", + "formatted": "SELECT TRY_CAST(c1:[\"foo bar\"][\"baz qux\"] AS ARRAY)" + }, + { + "dialect": "databricks", + "sql": "SELECT c1:item[1].price FROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)", + "formatted": "SELECT c1:item[1].price\nFROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT c1:item[*].price FROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)", + "formatted": "SELECT c1:item[*].price\nFROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT FROM_JSON(c1:item[*].price, 'ARRAY')[0] FROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)", + "formatted": "SELECT FROM_JSON(c1:item[*].price, 'ARRAY')[0]\nFROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT INLINE(FROM_JSON(c1:item[*], 'ARRAY>')) FROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)", + "formatted": "SELECT INLINE(FROM_JSON(c1:item[*], 'ARRAY>'))\nFROM VALUES ('{ \"item\": [ { \"model\" : \"basic\", \"price\" : 6.12 }, { \"model\" : \"medium\", \"price\" : 9.24 } ] }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT c1:['price'] FROM VALUES ('{ \"price\": 5 }') AS T(c1)", + "formatted": "SELECT c1:['price']\nFROM VALUES ('{ \"price\": 5 }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT GET_JSON_OBJECT(c1, '$.price') FROM VALUES ('{ \"price\": 5 }') AS T(c1)", + "formatted": "SELECT GET_JSON_OBJECT(c1, '$.price')\nFROM VALUES ('{ \"price\": 5 }') AS T(c1)" + }, + { + "dialect": "databricks", + "sql": "SELECT GET_JSON_OBJECT(col, path_col)", + "formatted": "SELECT GET_JSON_OBJECT(col, path_col)" + }, + { + "dialect": "databricks", + "sql": "SELECT GET_JSON_OBJECT(col, CONCAT('$.', field_name))", + "formatted": "SELECT GET_JSON_OBJECT(col, CONCAT('$.', field_name))" + }, + { + "dialect": "databricks", + "sql": "SELECT GET_JSON_OBJECT(GET_JSON_OBJECT(col, '$[0]'), '$.a')", + "formatted": "SELECT GET_JSON_OBJECT(GET_JSON_OBJECT(col, '$[0]'), '$.a')" + }, + { + "dialect": "databricks", + "sql": "SELECT raw:`zip code`, raw:`fb:testid`, raw:store['bicycle'], raw:store[\"zip code\"]", + "formatted": "SELECT\n raw:`zip code`,\n raw:`fb:testid`,\n raw:store['bicycle'],\n raw:store[\"zip code\"]" + }, + { + "dialect": "databricks", + "sql": "SELECT DATE_ADD(MONTH, 1, '2020-01-01')", + "formatted": "SELECT DATE_ADD(MONTH, 1, '2020-01-01')" + }, + { + "dialect": "databricks", + "sql": "SELECT DATEADD(e, 24) FROM t", + "formatted": "SELECT DATEADD(e, 24)\nFROM t" + }, + { + "dialect": "databricks", + "sql": "SELECT '20'?::INTEGER", + "formatted": "SELECT '20'?::INTEGER" + }, + { + "dialect": "databricks", + "sql": "SELECT OVERLAY('Spark SQL', 'ANSI ', 7, 0)", + "formatted": "SELECT OVERLAY('Spark SQL', 'ANSI ', 7, 0)" + }, + { + "dialect": "databricks", + "sql": "SELECT OVERLAY('Spark SQL' PLACING 'CORE' FROM 7)", + "formatted": "SELECT OVERLAY('Spark SQL' PLACING 'CORE' FROM 7)" + }, + { + "dialect": "databricks", + "sql": "SELECT OVERLAY(ENCODE('Spark SQL', 'utf-8') PLACING ENCODE('_', 'utf-8') FROM 6)", + "formatted": "SELECT OVERLAY(ENCODE('Spark SQL', 'utf-8') PLACING ENCODE('_', 'utf-8') FROM 6)" + }, + { + "dialect": "databricks", + "sql": "SELECT OVERLAY('Spark SQL' PLACING 'ANSI ' FROM 7 FOR 0)", + "formatted": "SELECT OVERLAY('Spark SQL' PLACING 'ANSI ' FROM 7 FOR 0)" + }, + { + "dialect": "databricks", + "sql": "WITH t AS (SELECT '{\"x-y\": \"z\"}' AS c) SELECT get_json_object(c, '$.x-y') FROM t", + "formatted": "WITH t AS (\n SELECT '{\"x-y\": \"z\"}' AS c\n)\n\nSELECT get_json_object(c, '$.x-y')\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT ([1,2,3])[:-:-1]", + "formatted": "SELECT\n (\n [1, 2, 3]\n )[:-:-1]" + }, + { + "dialect": "duckdb", + "sql": "SELECT INTERVAL '1 hour'::VARCHAR", + "formatted": "SELECT INTERVAL '1 hour'::VARCHAR" + }, + { + "dialect": "duckdb", + "sql": "SELECT str[0:1]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT COSH(1.5)", + "formatted": "SELECT COSH(1.5)" + }, + { + "dialect": "duckdb", + "sql": "SELECT MODE(category)", + "formatted": "SELECT MODE(category)" + }, + { + "dialect": "duckdb", + "sql": "SELECT e'\\n'", + "formatted": "SELECT e'\\n'" + }, + { + "dialect": "duckdb", + "sql": "SELECT e'\\t'", + "formatted": "SELECT e'\\t'" + }, + { + "dialect": "duckdb", + "sql": "SELECT e'update table_name set a = \\'foo\\' where 1 = 0' AS x FROM tab", + "formatted": "SELECT e'update table_name set a = \\'foo\\' where 1 = 0' AS x\nFROM tab" + }, + { + "dialect": "duckdb", + "sql": "SELECT GET_BIT(CAST('0110010' AS BIT), 2)", + "formatted": "SELECT GET_BIT(CAST('0110010' AS BIT), 2)" + }, + { + "dialect": "duckdb", + "sql": "SELECT 1 WHERE x > $1", + "formatted": "SELECT 1\nWHERE x > $1" + }, + { + "dialect": "duckdb", + "sql": "SELECT 1 WHERE x > $name", + "formatted": "SELECT 1\nWHERE x > $name" + }, + { + "dialect": "duckdb", + "sql": "SELECT 1 AS a$b", + "formatted": "SELECT 1 AS a$b" + }, + { + "dialect": "duckdb", + "sql": "SELECT '{\"x\": 1}' -> c FROM t", + "formatted": "SELECT '{\"x\": 1}' -> c\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "duckdb", + "sql": "SELECT '{\"duck\": [1, 2, 3]}' -> '$.duck[#-1]'", + "formatted": "SELECT '{\"duck\": [1, 2, 3]}' -> '$.duck[#-1]'" + }, + { + "dialect": "duckdb", + "sql": "SELECT tbl.x*1e4+tbl.y FROM tbl", + "formatted": "SELECT tbl.x * 1e4 + tbl.y\nFROM tbl" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_TRANSFORM([5, NULL, 6], (x, y) -> COALESCE(x, y, 0) + 1)", + "formatted": "SELECT LIST_TRANSFORM([5, NULL, 6], (x, y) -> COALESCE(x, y, 0) + 1)" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_TRANSFORM([5, NULL, 6], LAMBDA x, y : COALESCE(x, y, 0) + 1)", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_TRANSFORM(LIST_FILTER([0, 1, 2, 3, 4, 5], LAMBDA x : x % 2 = 0), LAMBDA y : y * y)", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_TRANSFORM([5, NULL, 6], LAMBDA x : COALESCE(x, 0) + 1)", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_TRANSFORM(nbr, LAMBDA x : x + 1) FROM article AS a", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM my_ducklake.demo AT (VERSION => 2)", + "formatted": "SELECT *\nFROM my_ducklake.demo AT (VERSION => 2)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t1 AS a AT (VERSION => 3) JOIN t2 AS b AT (TIMESTAMP => NOW() - INTERVAL '1' WEEK) ON a.id = b.id", + "formatted": "SELECT *\nFROM t1 AS a AT (VERSION => 3)\nJOIN t2 AS b AT (TIMESTAMP => NOW() - INTERVAL '1' WEEK)\n ON a.id = b.id" + }, + { + "dialect": "duckdb", + "sql": "SELECT TO_BINARY('test')", + "formatted": "SELECT TO_BINARY('test')" + }, + { + "dialect": "duckdb", + "sql": "SELECT UUIDV7()", + "formatted": "SELECT UUIDV7()" + }, + { + "dialect": "duckdb", + "sql": "SELECT TRY(LOG(0))", + "formatted": "SELECT TRY(LOG(0))" + }, + { + "dialect": "duckdb", + "sql": "SELECT 1 AS x UNION ALL BY NAME SELECT 2 AS x", + "formatted": "SELECT 1 AS x\nUNION ALL BY NAME\nSELECT 2 AS x" + }, + { + "dialect": "duckdb", + "sql": "SELECT SUM(x) FILTER (x = 1)", + "formatted": "SELECT SUM(x) FILTER(x = 1)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM GLOB(x)", + "formatted": "SELECT *\nFROM GLOB(x)" + }, + { + "dialect": "duckdb", + "sql": "SELECT MAP(['key1', 'key2', 'key3'], [10, 20, 30])", + "formatted": "SELECT\n MAP(\n ['key1', 'key2', 'key3'],\n [10, 20, 30]\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT MAP {'x': 1}", + "formatted": "SELECT MAP {'x': 1}" + }, + { + "dialect": "duckdb", + "sql": "SELECT (MAP {'x': 1})['x']", + "formatted": "SELECT\n (\n MAP {'x': 1}\n )['x']" + }, + { + "dialect": "duckdb", + "sql": "SELECT df1.*, df2.* FROM df1 POSITIONAL JOIN df2", + "formatted": "SELECT\n df1.*,\n df2.*\nFROM df1\nPOSITIONAL JOIN df2" + }, + { + "dialect": "duckdb", + "sql": "SELECT EPOCH_MS(10) AS t", + "formatted": "SELECT EPOCH_MS(10) AS t" + }, + { + "dialect": "duckdb", + "sql": "SELECT MAKE_TIMESTAMP(10) AS t", + "formatted": "SELECT MAKE_TIMESTAMP(10) AS t" + }, + { + "dialect": "duckdb", + "sql": "SELECT TO_TIMESTAMP(10) AS t", + "formatted": "SELECT TO_TIMESTAMP(10) AS t" + }, + { + "dialect": "duckdb", + "sql": "SELECT UNNEST(col, recursive := TRUE) FROM t", + "formatted": "SELECT UNNEST(col, recursive := TRUE)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo ASOF LEFT JOIN bar ON a = b", + "formatted": "SELECT *\nFROM foo\nASOF LEFT JOIN bar\n ON a = b" + }, + { + "dialect": "duckdb", + "sql": "SELECT {'a': 1} AS x", + "formatted": "SELECT {'a': 1} AS x" + }, + { + "dialect": "duckdb", + "sql": "SELECT {'a': {'b': {'c': 1}}, 'd': {'e': 2}} AS x", + "formatted": "SELECT {'a': {'b': {'c': 1}}, 'd': {'e': 2}} AS x" + }, + { + "dialect": "duckdb", + "sql": "SELECT {'x': 1, 'y': 2, 'z': 3}", + "formatted": "SELECT {'x': 1, 'y': 2, 'z': 3}" + }, + { + "dialect": "duckdb", + "sql": "SELECT {'key1': 'string', 'key2': 1, 'key3': 12.345}", + "formatted": "SELECT {'key1': 'string', 'key2': 1, 'key3': 12.345}" + }, + { + "dialect": "duckdb", + "sql": "SELECT ROW(x, x + 1, y) FROM (SELECT 1 AS x, 'a' AS y)", + "formatted": "SELECT ROW(x, x + 1, y)\nFROM (\n SELECT\n 1 AS x,\n 'a' AS y\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT (x, x + 1, y) FROM (SELECT 1 AS x, 'a' AS y)", + "formatted": "SELECT (x, x + 1, y)\nFROM (\n SELECT\n 1 AS x,\n 'a' AS y\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT a.x FROM (SELECT {'x': 1, 'y': 2, 'z': 3} AS a)", + "formatted": "SELECT a.x\nFROM (\n SELECT {'x': 1, 'y': 2, 'z': 3} AS a\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t1 WHERE NOT EXISTS(FROM t2 WHERE t2.id = t1.id)", + "formatted": "SELECT *\nFROM t1\nWHERE\n NOT EXISTS(\n FROM t2\n WHERE t2.id = t1.id\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo WHERE bar > $baz AND bla = $bob", + "formatted": "SELECT *\nFROM foo\nWHERE\n bar > $baz AND bla = $bob" + }, + { + "dialect": "duckdb", + "sql": "SELECT STAR(tbl, exclude := [foo])", + "formatted": "SELECT STAR(tbl, exclude := [foo])" + }, + { + "dialect": "duckdb", + "sql": "SELECT species, island, COUNT(*) FROM t GROUP BY GROUPING SETS (species), GROUPING SETS (island)", + "formatted": "SELECT\n species,\n island,\n COUNT(*)\nFROM t\nGROUP BY\n GROUPING SETS (\n species\n ),\n GROUPING SETS (\n island\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT species, island, COUNT(*) FROM t GROUP BY CUBE (species), CUBE (island)", + "formatted": "SELECT\n species,\n island,\n COUNT(*)\nFROM t\nGROUP BY\n CUBE (\n species\n ),\n CUBE (\n island\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT species, island, COUNT(*) FROM t GROUP BY ROLLUP (species), ROLLUP (island)", + "formatted": "SELECT\n species,\n island,\n COUNT(*)\nFROM t\nGROUP BY\n ROLLUP (\n species\n ),\n ROLLUP (\n island\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT '{ \"family\": \"anatidae\", \"species\": [ \"duck\", \"goose\", \"swan\", null ] }' ->> ['$.family', '$.species']", + "formatted": "SELECT '{ \"family\": \"anatidae\", \"species\": [ \"duck\", \"goose\", \"swan\", null ] }' ->> ['$.family', '$.species']" + }, + { + "dialect": "duckdb", + "sql": "SELECT $🦆$foo$🦆$", + "formatted": "SELECT $🦆$foo$🦆$" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t PIVOT(FIRST(t) AS t, FOR quarter IN ('Q1', 'Q2'))", + "formatted": "SELECT *\nFROM t PIVOT(FIRST(t) AS t, FOR quarter IN ('Q1', 'Q2'))" + }, + { + "dialect": "duckdb", + "sql": "SELECT JSON_EXTRACT_STRING('{ \"family\": \"anatidae\", \"species\": [ \"duck\", \"goose\", \"swan\", null ] }', ['$.family', '$.species'])", + "formatted": "SELECT JSON_EXTRACT_STRING('{ \"family\": \"anatidae\", \"species\": [ \"duck\", \"goose\", \"swan\", null ] }', ['$.family', '$.species'])" + }, + { + "dialect": "duckdb", + "sql": "SELECT col FROM t WHERE JSON_EXTRACT_STRING(col, '$.id') NOT IN ('b')", + "formatted": "SELECT col\nFROM t\nWHERE JSON_EXTRACT_STRING(col, '$.id') NOT IN ('b')" + }, + { + "dialect": "duckdb", + "sql": "SELECT a, LOGICAL_OR(b) FROM foo GROUP BY a", + "formatted": "SELECT\n a,\n LOGICAL_OR(b)\nFROM foo\nGROUP BY\n a" + }, + { + "dialect": "duckdb", + "sql": "SELECT JSON_EXTRACT_STRING(c, '$.k1') = 'v1'", + "formatted": "SELECT JSON_EXTRACT_STRING(c, '$.k1') = 'v1'" + }, + { + "dialect": "duckdb", + "sql": "SELECT JSON_EXTRACT(c, '$.k1') = 'v1'", + "formatted": "SELECT JSON_EXTRACT(c, '$.k1') = 'v1'" + }, + { + "dialect": "duckdb", + "sql": "SELECT JSON_EXTRACT(c, '$[*].id')[0:2]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT JSON_EXTRACT_STRING(c, '$[*].id')[0:2]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT '{\"foo\": [1, 2, 3]}' -> 'foo' -> 0", + "formatted": "SELECT '{\"foo\": [1, 2, 3]}' -> 'foo' -> 0" + }, + { + "dialect": "duckdb", + "sql": "SELECT ($$hello)'world$$)", + "formatted": "SELECT\n (\n $$hello)'world$$\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT $$foo$$", + "formatted": "SELECT $$foo$$" + }, + { + "dialect": "duckdb", + "sql": "SELECT $tag$foo$tag$", + "formatted": "SELECT $tag$foo$tag$" + }, + { + "dialect": "duckdb", + "sql": "SELECT {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'}", + "formatted": "SELECT {'yes': 'duck', 'maybe': 'goose', 'huh': NULL, 'no': 'heron'}" + }, + { + "dialect": "duckdb", + "sql": "SELECT a['x space'] FROM (SELECT {'x space': 1, 'y': 2, 'z': 3} AS a)", + "formatted": "SELECT a['x space']\nFROM (\n SELECT {'x space': 1, 'y': 2, 'z': 3} AS a\n)" + }, + { + "dialect": "duckdb", + "sql": "WITH pivot_alias AS (PIVOT Cities ON Year USING SUM(Population) GROUP BY Country) SELECT * FROM pivot_alias", + "formatted": "WITH pivot_alias AS (\n PIVOT Cities ON Year USING SUM(Population) GROUP BY Country\n)\n\nSELECT *\nFROM pivot_alias" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM (PIVOT Cities ON Year USING SUM(Population) GROUP BY Country) AS pivot_alias", + "formatted": "SELECT *\nFROM (PIVOT Cities ON Year USING SUM(Population) GROUP BY Country) AS pivot_alias" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM cities PIVOT(SUM(population) FOR year IN (2000, 2010, 2020) GROUP BY country)", + "formatted": "SELECT *\nFROM cities PIVOT(SUM(population) FOR year IN (2000, 2010, 2020) GROUP BY country)" + }, + { + "dialect": "duckdb", + "sql": "SELECT schema_name, function_name, ROW_NUMBER() OVER my_window AS function_rank FROM DUCKDB_FUNCTIONS() WINDOW my_window AS (PARTITION BY schema_name ORDER BY function_name) QUALIFY ROW_NUMBER() OVER my_window < 3", + "formatted": "SELECT\n schema_name,\n function_name,\n ROW_NUMBER() OVER my_window AS function_rank\nFROM DUCKDB_FUNCTIONS()\nWINDOW\n my_window AS (PARTITION BY schema_name ORDER BY function_name)\nQUALIFY ROW_NUMBER() OVER my_window < 3" + }, + { + "dialect": "duckdb", + "sql": "SELECT SHA256('abc')", + "formatted": "SELECT SHA256('abc')" + }, + { + "dialect": "duckdb", + "sql": "SELECT SPLIT_PART('11.22.33', '.', 1)", + "formatted": "SELECT SPLIT_PART('11.22.33', '.', 1)" + }, + { + "dialect": "duckdb", + "sql": "SELECT NTH_VALUE(is_deleted, 2) OVER (PARTITION BY id) AS nth_is_deleted FROM my_table", + "formatted": "SELECT NTH_VALUE(is_deleted, 2) OVER (PARTITION BY id) AS nth_is_deleted\nFROM my_table" + }, + { + "dialect": "duckdb", + "sql": "SELECT NTH_VALUE(is_deleted, 2 IGNORE NULLS) OVER (PARTITION BY id) AS nth_is_deleted FROM my_table", + "formatted": "SELECT NTH_VALUE(is_deleted, 2 IGNORE NULLS) OVER (PARTITION BY id) AS nth_is_deleted\nFROM my_table" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_MAX(values) FROM table1", + "formatted": "SELECT LIST_MAX(values)\nFROM table1" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_MIN(values) FROM table1", + "formatted": "SELECT LIST_MIN(values)\nFROM table1" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'ASC')", + "formatted": "SELECT LIST_SORT(x, 'ASC')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DESC')", + "formatted": "SELECT LIST_SORT(x, 'DESC')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'ASC', 'NULLS FIRST')", + "formatted": "SELECT LIST_SORT(x, 'ASC', 'NULLS FIRST')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'ASC', 'NULLS LAST')", + "formatted": "SELECT LIST_SORT(x, 'ASC', 'NULLS LAST')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DESC', 'NULLS FIRST')", + "formatted": "SELECT LIST_SORT(x, 'DESC', 'NULLS FIRST')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DESC', 'NULLS LAST')", + "formatted": "SELECT LIST_SORT(x, 'DESC', 'NULLS LAST')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DE' || 'SC')", + "formatted": "SELECT LIST_SORT(x, 'DE' || 'SC')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DESC', 'NULLS' || ' FIRST')", + "formatted": "SELECT LIST_SORT(x, 'DESC', 'NULLS' || ' FIRST')" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_SORT(x, 'DE' || 'SC', 'NULLS' || ' FIRST')", + "formatted": "SELECT\n LIST_SORT(\n x,\n 'DE' || 'SC',\n 'NULLS' || ' FIRST'\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY y DESC) FROM t", + "formatted": "SELECT PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY y DESC)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY y DESC) FROM t", + "formatted": "SELECT PERCENTILE_DISC(0.25) WITHIN GROUP (ORDER BY y DESC)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT(a, 'pattern', 0)", + "formatted": "SELECT REGEXP_EXTRACT(a, 'pattern', 0)" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT(a, 'pattern', 0, 'i')", + "formatted": "SELECT REGEXP_EXTRACT(a, 'pattern', 0, 'i')" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT(a, 'pattern', 1, 'i')", + "formatted": "SELECT REGEXP_EXTRACT(a, 'pattern', 1, 'i')" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0)", + "formatted": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0)" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 1)", + "formatted": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 1)" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0, 'i')", + "formatted": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0, 'i')" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 1, 'im')", + "formatted": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 1, 'im')" + }, + { + "dialect": "duckdb", + "sql": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0)[2:]", + "formatted": "SELECT REGEXP_EXTRACT_ALL(s, 'pattern', 0)[2:]" + }, + { + "dialect": "duckdb", + "sql": "SELECT ISNAN(x)", + "formatted": "SELECT ISNAN(x)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM RANGE(1, 5, 10)", + "formatted": "SELECT *\nFROM RANGE(1, 5, 10)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM GENERATE_SERIES(2, 13, 4)", + "formatted": "SELECT *\nFROM GENERATE_SERIES(2, 13, 4)" + }, + { + "dialect": "duckdb", + "sql": "SELECT i FROM RANGE(5) AS _(i) ORDER BY i ASC", + "formatted": "SELECT i\nFROM RANGE(5) AS _(i)\nORDER BY\n i ASC" + }, + { + "dialect": "duckdb", + "sql": "SELECT i FROM GENERATE_SERIES(12) AS _(i) ORDER BY i ASC", + "formatted": "SELECT i\nFROM GENERATE_SERIES(12) AS _(i)\nORDER BY\n i ASC" + }, + { + "dialect": "duckdb", + "sql": "SELECT LENGTH(foo)", + "formatted": "SELECT LENGTH(foo)" + }, + { + "dialect": "duckdb", + "sql": "SELECT ARRAY[1, 2, 3]", + "formatted": "SELECT ARRAY[1, 2, 3]" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM (DESCRIBE t)", + "formatted": "SELECT *\nFROM (DESCRIBE t)" + }, + { + "dialect": "duckdb", + "sql": "SELECT UNNEST([*COLUMNS('alias_.*')]) AS column_name", + "formatted": "SELECT UNNEST([*COLUMNS('alias_.*')]) AS column_name" + }, + { + "dialect": "duckdb", + "sql": "SELECT COALESCE(*COLUMNS(*)) FROM (SELECT NULL, 2, 3) AS t(a, b, c)", + "formatted": "SELECT COALESCE(*COLUMNS(*))\nFROM (\n SELECT\n NULL,\n 2,\n 3\n) AS t(a, b, c)" + }, + { + "dialect": "duckdb", + "sql": "SELECT id, STRUCT_PACK(*COLUMNS('m\\d')) AS measurements FROM many_measurements", + "formatted": "SELECT\n id,\n STRUCT_PACK(*COLUMNS('m\\d')) AS measurements\nFROM many_measurements" + }, + { + "dialect": "duckdb", + "sql": "SELECT COLUMNS(c -> c LIKE '%num%') FROM numbers", + "formatted": "SELECT COLUMNS(c -> c LIKE '%num%')\nFROM numbers" + }, + { + "dialect": "duckdb", + "sql": "SELECT MIN(COLUMNS(* REPLACE (number + id AS number))), COUNT(COLUMNS(* EXCLUDE (number))) FROM numbers", + "formatted": "SELECT\n MIN(COLUMNS(* REPLACE (number + id AS number))),\n COUNT(COLUMNS(* EXCLUDE (number)))\nFROM numbers" + }, + { + "dialect": "duckdb", + "sql": "SELECT COLUMNS(*) + COLUMNS(*) FROM numbers", + "formatted": "SELECT COLUMNS(*) + COLUMNS(*)\nFROM numbers" + }, + { + "dialect": "duckdb", + "sql": "SELECT COLUMNS('(id|numbers?)') FROM numbers", + "formatted": "SELECT COLUMNS('(id|numbers?)')\nFROM numbers" + }, + { + "dialect": "duckdb", + "sql": "SELECT COALESCE(COLUMNS(['a', 'b', 'c'])) AS result FROM (SELECT NULL AS a, 42 AS b, TRUE AS c)", + "formatted": "SELECT COALESCE(COLUMNS(['a', 'b', 'c'])) AS result\nFROM (\n SELECT\n NULL AS a,\n 42 AS b,\n TRUE AS c\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT COALESCE(*COLUMNS(['a', 'b', 'c'])) AS result FROM (SELECT NULL AS a, 42 AS b, TRUE AS c)", + "formatted": "SELECT COALESCE(*COLUMNS(['a', 'b', 'c'])) AS result\nFROM (\n SELECT\n NULL AS a,\n 42 AS b,\n TRUE AS c\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT DATE_ADD(CAST('2020-01-01' AS DATE), INTERVAL 1 DAY)", + "formatted": "SELECT DATE_ADD(CAST('2020-01-01' AS DATE), INTERVAL 1 DAY)" + }, + { + "dialect": "duckdb", + "sql": "SELECT #2, #1 FROM (VALUES (1, 'foo'))", + "formatted": "SELECT\n #2,\n #1\nFROM (VALUES (1, 'foo'))" + }, + { + "dialect": "duckdb", + "sql": "SELECT #2 AS a, #1 AS b FROM (VALUES (1, 'foo'))", + "formatted": "SELECT\n #2 AS a,\n #1 AS b\nFROM (VALUES (1, 'foo'))" + }, + { + "dialect": "duckdb", + "sql": "SELECT CUME_DIST( ORDER BY foo) OVER (ORDER BY 1) FROM (SELECT 1 AS foo)", + "formatted": "SELECT CUME_DIST(ORDER BY foo) OVER (ORDER BY 1)\nFROM (\n SELECT 1 AS foo\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT NTILE(1 ORDER BY foo) OVER (ORDER BY 1) FROM (SELECT 1 AS foo)", + "formatted": "SELECT NTILE(1 ORDER BY foo) OVER (ORDER BY 1)\nFROM (\n SELECT 1 AS foo\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT RANK( ORDER BY foo) OVER (ORDER BY 1) FROM (SELECT 1 AS foo)", + "formatted": "SELECT RANK(ORDER BY foo) OVER (ORDER BY 1)\nFROM (\n SELECT 1 AS foo\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT PERCENT_RANK( ORDER BY foo) OVER (ORDER BY 1) FROM (SELECT 1 AS foo)", + "formatted": "SELECT PERCENT_RANK(ORDER BY foo) OVER (ORDER BY 1)\nFROM (\n SELECT 1 AS foo\n)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t LIMIT 10 PERCENT", + "formatted": "SELECT *\nFROM t\nLIMIT 10 PERCENT" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t LIMIT 10%", + "formatted": "SELECT *\nFROM t\nLIMIT 10%", + "previously_formatted": "SELECT\n *\nFROM t\nLIMIT 10 %" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t LIMIT 10 PERCENT OFFSET 1", + "formatted": "SELECT *\nFROM t\nLIMIT 10 PERCENT\nOFFSET 1" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t LIMIT 10% OFFSET 1", + "formatted": "SELECT *\nFROM t\nLIMIT 10%\nOFFSET 1", + "previously_formatted": "SELECT\n *\nFROM t\nLIMIT 10 %\nOFFSET 1" + }, + { + "dialect": "duckdb", + "sql": "SELECT CAST(ROW(1, 2) AS ROW(a INTEGER, b INTEGER))", + "formatted": "SELECT CAST(ROW(1, 2) AS ROW(a INTEGER, b INTEGER))" + }, + { + "dialect": "duckdb", + "sql": "SELECT row", + "formatted": "SELECT row" + }, + { + "dialect": "duckdb", + "sql": "SELECT TRY_STRPTIME('2013-04-28T20:57:01.123456789+07:00', '%Y-%m-%dT%H:%M:%S.%n%z')", + "formatted": "SELECT TRY_STRPTIME('2013-04-28T20:57:01.123456789+07:00', '%Y-%m-%dT%H:%M:%S.%n%z')" + }, + { + "dialect": "duckdb", + "sql": "SELECT REPLACE('apple pie', 'pie', 'cobbler') AS result", + "formatted": "SELECT REPLACE('apple pie', 'pie', 'cobbler') AS result" + }, + { + "dialect": "duckdb", + "sql": "SELECT REPLACE(CAST(CAST('apple pie' AS BLOB) AS TEXT), CAST(CAST('pie' AS BLOB) AS TEXT), CAST(CAST('cobbler' AS BLOB) AS TEXT)) AS result", + "formatted": "SELECT\n REPLACE(\n CAST(CAST('apple pie' AS BLOB) AS TEXT),\n CAST(CAST('pie' AS BLOB) AS TEXT),\n CAST(CAST('cobbler' AS BLOB) AS TEXT)\n ) AS result" + }, + { + "dialect": "duckdb", + "sql": "SELECT TRIM('***apple***', '*') AS result", + "formatted": "SELECT TRIM('***apple***', '*') AS result" + }, + { + "dialect": "duckdb", + "sql": "SELECT CAST(TRIM(CAST(CAST('***apple***' AS BLOB) AS TEXT), CAST(CAST('*' AS BLOB) AS TEXT)) AS BLOB) AS result", + "formatted": "SELECT CAST(TRIM(CAST(CAST('***apple***' AS BLOB) AS TEXT), CAST(CAST('*' AS BLOB) AS TEXT)) AS BLOB) AS result" + }, + { + "dialect": "duckdb", + "sql": "SELECT GREATEST(1.0, 2.5, NULL, 3.7)", + "formatted": "SELECT GREATEST(1.0, 2.5, NULL, 3.7)" + }, + { + "dialect": "duckdb", + "sql": "SELECT MAKE_DATE(DATE_PART(['year', 'month', 'day'], CURRENT_DATE))", + "formatted": "SELECT\n MAKE_DATE(\n DATE_PART(\n ['year', 'month', 'day'],\n CURRENT_DATE\n )\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM t PIVOT(SUM(y) FOR foo IN y_enum)", + "formatted": "SELECT *\nFROM t PIVOT(SUM(y) FOR foo IN y_enum)" + }, + { + "dialect": "duckdb", + "sql": "SELECT 20_000 AS literal", + "formatted": "SELECT 20_000 AS literal" + }, + { + "dialect": "duckdb", + "sql": "SELECT 1_2E+1_0::FLOAT", + "formatted": "SELECT 1_2E+1_0::FLOAT" + }, + { + "dialect": "duckdb", + "sql": "SELECT [1, 2, 3][1 + 1:LENGTH([1, 2, 3]) + -1]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT TODAY()", + "formatted": "SELECT TODAY()" + }, + { + "dialect": "duckdb", + "sql": "SELECT GET_CURRENT_TIME()", + "formatted": "SELECT GET_CURRENT_TIME()" + }, + { + "dialect": "duckdb", + "sql": "SELECT SUM(x) OVER (ORDER BY x GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW) FROM t", + "formatted": "SELECT SUM(x) OVER (ORDER BY x GROUPS BETWEEN 1 PRECEDING AND CURRENT ROW)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT LAST_VALUE(x ORDER BY x IGNORE NULLS) OVER (ORDER BY x) FROM t", + "formatted": "SELECT LAST_VALUE(x ORDER BY x IGNORE NULLS) OVER (ORDER BY x)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT LAST_VALUE(x ORDER BY x RESPECT NULLS) OVER (ORDER BY x) FROM t", + "formatted": "SELECT LAST_VALUE(x ORDER BY x RESPECT NULLS) OVER (ORDER BY x)\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT CURRENT_DATE", + "formatted": "SELECT CURRENT_DATE" + }, + { + "dialect": "duckdb", + "sql": "SELECT CURRENT_TIMESTAMP", + "formatted": "SELECT CURRENT_TIMESTAMP" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl USING SAMPLE 5", + "formatted": "SELECT *\nFROM tbl\nUSING SAMPLE 5" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl USING SAMPLE 10%", + "formatted": "SELECT *\nFROM tbl\nUSING SAMPLE 10%" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl USING SAMPLE 10 PERCENT (bernoulli)", + "formatted": "SELECT *\nFROM tbl\nUSING SAMPLE 10 PERCENT (bernoulli)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl USING SAMPLE reservoir(50 ROWS) REPEATABLE (100)", + "formatted": "SELECT *\nFROM tbl\nUSING SAMPLE reservoir (50 ROWS) REPEATABLE (100)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl USING SAMPLE 10% (system, 377)", + "formatted": "SELECT *\nFROM tbl\nUSING SAMPLE 10% (system, 377)" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl TABLESAMPLE RESERVOIR(20%), tbl2 WHERE tbl.i=tbl2.i", + "formatted": "SELECT *\nFROM tbl TABLESAMPLE RESERVOIR (20%), tbl2\nWHERE tbl.i = tbl2.i", + "previously_formatted": "SELECT\n *\nFROM tbl TABLESAMPLE RESERVOIR (20 %), tbl2\nWHERE\n tbl.i = tbl2.i" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM tbl, tbl2 WHERE tbl.i=tbl2.i USING SAMPLE RESERVOIR(20%)", + "formatted": "SELECT *\nFROM tbl, tbl2\nWHERE tbl.i = tbl2.i\nUSING SAMPLE RESERVOIR (20%)", + "previously_formatted": "SELECT\n *\nFROM tbl, tbl2\nWHERE\n tbl.i = tbl2.i\nUSING SAMPLE RESERVOIR (20 %)" + }, + { + "dialect": "duckdb", + "sql": "SELECT x::INT[3][3]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT ARRAY[[[1]]]::INT[1][1][1]", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "WITH unpivot_alias AS (UNPIVOT monthly_sales ON COLUMNS(* EXCLUDE (empid, dept)) INTO NAME month VALUE sales) SELECT * FROM unpivot_alias", + "formatted": "WITH unpivot_alias AS (\n UNPIVOT monthly_sales ON COLUMNS(* EXCLUDE (empid, dept)) INTO NAME month VALUE sales\n)\n\nSELECT *\nFROM unpivot_alias" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM (UNPIVOT monthly_sales ON COLUMNS(* EXCLUDE (empid, dept)) INTO NAME month VALUE sales) AS unpivot_alias", + "formatted": "SELECT *\nFROM (UNPIVOT monthly_sales ON COLUMNS(* EXCLUDE (empid, dept)) INTO NAME month VALUE sales) AS unpivot_alias" + }, + { + "dialect": "duckdb", + "sql": "WITH cities(country, name, year, population) AS (SELECT 'NL', 'Amsterdam', 2000, 1005 UNION ALL SELECT 'US', 'Seattle', 2020, 738) PIVOT cities ON year USING SUM(population)", + "formatted": "WITH cities(country, name, year, population) AS (\n SELECT\n 'NL',\n 'Amsterdam',\n 2000,\n 1005\n UNION ALL\n SELECT\n 'US',\n 'Seattle',\n 2020,\n 738\n)\n\nPIVOT cities ON year USING SUM(population)" + }, + { + "dialect": "duckdb", + "sql": "WITH t1 AS (FROM (FROM t2 SELECT foo1, foo2)) FROM t1", + "formatted": "WITH t1 AS (\n FROM (FROM t2\n SELECT\n foo1,\n foo2\n )\n)\n\nFROM t1" + }, + { + "dialect": "duckdb", + "sql": "SELECT foo: 1", + "formatted": "SELECT foo:1" + }, + { + "dialect": "duckdb", + "sql": "SELECT foo: bar", + "formatted": "SELECT foo:bar" + }, + { + "dialect": "duckdb", + "sql": "SELECT foo: t.col FROM t", + "formatted": "SELECT foo:t.col\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT \"foo\" /* bla */: 1", + "formatted": "SELECT\n \"foo\" /* bla */:1" + }, + { + "dialect": "duckdb", + "sql": "SELECT \"foo\": 1 /* bla */", + "formatted": "SELECT \"foo\":1 /* bla */" + }, + { + "dialect": "duckdb", + "sql": "SELECT \"foo\": /* bla */ 1", + "formatted": "SELECT\n \"foo\": /* bla */1" + }, + { + "dialect": "duckdb", + "sql": "SELECT \"foo\": /* bla */ 1 /* foo */", + "formatted": "SELECT\n \"foo\": /* bla */1 /* foo */" + }, + { + "dialect": "duckdb", + "sql": "SELECT \"foo\": 1", + "formatted": "SELECT \"foo\":1" + }, + { + "dialect": "duckdb", + "sql": "SELECT foo: 1, bar: 2, baz: 3", + "formatted": "SELECT\n foo:1,\n bar:2,\n baz:3" + }, + { + "dialect": "duckdb", + "sql": "SELECT e: 1 + 2, f: len('asdf'), s: (SELECT 42)", + "formatted": "SELECT\n e:1 + 2,\n f:len('asdf'), s:\n (\n SELECT 42\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo: bar", + "formatted": "SELECT *\nFROM foo:bar" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo: c.db.tbl", + "formatted": "SELECT *\nFROM foo:c.db.tbl" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo /* bla */: bar", + "formatted": "SELECT *\nFROM foo /* bla */:bar" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo /* bla */: bar /* baz */", + "formatted": "SELECT *\nFROM foo /* bla */:bar /* baz */" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM foo /* bla */: /* baz */ bar /* boo */", + "formatted": "SELECT *\nFROM foo /* bla */: /* baz */bar /* boo */" + }, + { + "dialect": "duckdb", + "sql": "SELECT * FROM r: range(10), v: (VALUES (42)), s: (FROM range(10))", + "formatted": "SELECT *\nFROM r:range(10), v:(VALUES (42)), s:(\n FROM RANGE(10)\n)" + }, + { + "dialect": "duckdb", + "sql": "\n SELECT\n l_returnflag,\n l_linestatus,\n sum_qty: sum(l_quantity),\n sum_base_price: sum(l_extendedprice),\n sum_disc_price: sum(l_extendedprice * (1-l_discount)),\n sum_charge: sum(l_extendedprice * (1-l_discount) * (1+l_tax)),\n avg_qty: avg(l_quantity),\n avg_price: avg(l_extendedprice),\n avg_disc: avg(l_discount),\n count_order: count(*)\n ", + "formatted": "SELECT\n l_returnflag,\n l_linestatus,\n sum_qty:sum(l_quantity),\n sum_base_price:sum(l_extendedprice),\n sum_disc_price:sum(l_extendedprice\n * (\n 1 - l_discount\n )),\n sum_charge:sum(l_extendedprice\n * (\n 1 - l_discount\n )\n * (\n 1 + l_tax\n )),\n avg_qty:avg(l_quantity),\n avg_price:avg(l_extendedprice),\n avg_disc:avg(l_discount),\n count_order:count(*)" + }, + { + "dialect": "duckdb", + "sql": "SELECT @col FROM t", + "formatted": "SELECT @col\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT @col + 1 FROM t", + "formatted": "SELECT @col + 1\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT (@col) + 1 FROM t", + "formatted": "SELECT\n (@col\n )\n + 1\nFROM t" + }, + { + "dialect": "duckdb", + "sql": "SELECT @(-1)", + "formatted": "SELECT\n @(\n -1\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT @(-1) + 1", + "formatted": "SELECT\n @(\n -1\n )\n + 1" + }, + { + "dialect": "duckdb", + "sql": "SELECT (@-1) + 1", + "formatted": "SELECT\n (@-1\n )\n + 1" + }, + { + "dialect": "duckdb", + "sql": "WITH RECURSIVE tbl(a, b) USING KEY (a) AS (SELECT a, b FROM (VALUES (1, 3), (2, 4)) AS t(a, b) UNION SELECT a + 1, b FROM tbl WHERE a < 3) SELECT * FROM tbl", + "formatted": "WITH RECURSIVE tbl(a, b) USING KEY (a) AS (\n SELECT\n a,\n b\n FROM (VALUES (1, 3), (2, 4)) AS t(a, b)\n UNION\n SELECT\n a + 1,\n b\n FROM tbl\n WHERE a < 3\n)\nSELECT *\nFROM tbl" + }, + { + "dialect": "duckdb", + "sql": "WITH RECURSIVE tbl(a, b) USING KEY (a, b) AS (SELECT a, b FROM (VALUES (1, 3), (2, 4)) AS t(a, b) UNION SELECT a + 1, b FROM tbl WHERE a < 3) SELECT * FROM tbl", + "formatted": "WITH RECURSIVE tbl(a, b) USING KEY (a, b) AS (\n SELECT\n a,\n b\n FROM (VALUES (1, 3), (2, 4)) AS t(a, b)\n UNION\n SELECT\n a + 1,\n b\n FROM tbl\n WHERE a < 3\n)\nSELECT *\nFROM tbl" + }, + { + "dialect": "duckdb", + "sql": "WITH _data AS (SELECT [{'a': 1, 'b': 2}, {'a': 2, 'b': 3}] AS col) SELECT t.col['b'] FROM _data, UNNEST(_data.col) AS t(col) WHERE t.col['a'] = 1", + "formatted": "WITH _data AS (\n SELECT\n [\n {'a': 1, 'b': 2},\n {'a': 2, 'b': 3}\n ] AS col\n)\n\nSELECT t.col['b']\nFROM _data, UNNEST(_data.col) AS t(col)\nWHERE t.col['a'] = 1" + }, + { + "dialect": "duckdb", + "sql": "SELECT [4, 5, 6] AS l, [x FOR x, i IN l IF i = 2] AS filtered", + "formatted": "SELECT\n [4, 5, 6] AS l,\n [x FOR x, i IN l IF i = 2] AS filtered" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_VALUE(1)[i]", + "formatted": "SELECT LIST_VALUE(1)[i]" + }, + { + "dialect": "duckdb", + "sql": "SELECT LIST_APPLY(RANGE(1, 4), i -> {'f1': LIST_VALUE(1, 2, 3)[i], 'f2': LIST_VALUE(1, 2, 3)[i]})", + "formatted": "SELECT\n LIST_APPLY(\n RANGE(1, 4),\n i -> {'f1': LIST_VALUE(1, 2, 3)[i], 'f2': LIST_VALUE(1, 2, 3)[i]}\n )" + }, + { + "dialect": "duckdb", + "sql": "SELECT UNNEST([1, 2])", + "formatted": "SELECT UNNEST([1, 2])" + }, + { + "dialect": "duckdb", + "sql": "SELECT file[:256] FROM GLOB('*')", + "formatted": null + }, + { + "dialect": "duckdb", + "sql": "SELECT file[256] FROM GLOB('*')", + "formatted": "SELECT file[256]\nFROM GLOB('*')" + }, + { + "dialect": "postgres", + "sql": "WITH t AS (SELECT 1 AS \"null\") SELECT t.null FROM t", + "formatted": "WITH t AS (\n SELECT 1 AS \"null\"\n)\n\nSELECT t.null\nFROM t" + }, + { + "dialect": "postgres", + "sql": "WITH t AS (SELECT 1 AS \"true\") SELECT t.true FROM t", + "formatted": "WITH t AS (\n SELECT 1 AS \"true\"\n)\n\nSELECT t.true\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT '%' SIMILAR TO '^%' ESCAPE '^'", + "formatted": "SELECT '%' SIMILAR TO '^%' ESCAPE '^'" + }, + { + "dialect": "postgres", + "sql": "SELECT GET_BIT(CAST(44 AS BIT(10)), 6)", + "formatted": "SELECT GET_BIT(CAST(44 AS BIT(10)), 6)" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM t GROUP BY ROLLUP (a || '^' || b)", + "formatted": "SELECT *\nFROM t\nGROUP BY\n ROLLUP (\n a || '^' || b\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT COSH(1.5)", + "formatted": "SELECT COSH(1.5)" + }, + { + "dialect": "postgres", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "postgres", + "sql": "SELECT MODE() WITHIN GROUP (ORDER BY status DESC) AS most_common FROM orders", + "formatted": "SELECT MODE() WITHIN GROUP (ORDER BY status DESC) AS most_common\nFROM orders" + }, + { + "dialect": "postgres", + "sql": "SELECT ST_DISTANCE(gg1, gg2, FALSE) AS sphere_dist", + "formatted": "SELECT ST_DISTANCE(gg1, gg2, FALSE) AS sphere_dist" + }, + { + "dialect": "postgres", + "sql": "SELECT EXTRACT(QUARTER FROM CAST('2025-04-26' AS DATE))", + "formatted": "SELECT EXTRACT(QUARTER FROM CAST('2025-04-26' AS DATE))" + }, + { + "dialect": "postgres", + "sql": "SELECT DATE_TRUNC('QUARTER', CAST('2025-04-26' AS DATE))", + "formatted": "SELECT DATE_TRUNC('QUARTER', CAST('2025-04-26' AS DATE))" + }, + { + "dialect": "postgres", + "sql": "SELECT x FROM t WHERE CAST($1 AS TEXT) = 'ok'", + "formatted": "SELECT x\nFROM t\nWHERE CAST($1 AS TEXT) = 'ok'" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM t TABLESAMPLE SYSTEM (50) REPEATABLE (55)", + "formatted": "SELECT *\nFROM t TABLESAMPLE SYSTEM (50) REPEATABLE (55)" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM events WHERE doc @? '$.a[*] ? (@ > 2)'", + "formatted": "SELECT *\nFROM events\nWHERE doc @? '$.a[*] ? (@ > 2)'" + }, + { + "dialect": "postgres", + "sql": "SELECT ARRAY[1, 2, 3]", + "formatted": "SELECT ARRAY[1, 2, 3]" + }, + { + "dialect": "postgres", + "sql": "SELECT ARRAY(SELECT 1)", + "formatted": "SELECT\n ARRAY(SELECT\n 1)" + }, + { + "dialect": "postgres", + "sql": "SELECT CASE WHEN SUBSTRING('abcdefg') IN ('ab') THEN 1 ELSE 0 END", + "formatted": "SELECT\n CASE\n WHEN SUBSTRING('abcdefg') IN ('ab')\n THEN 1\n ELSE 0\n END" + }, + { + "dialect": "postgres", + "sql": "SELECT e'\\xDEADBEEF'", + "formatted": "SELECT e'\\xDEADBEEF'" + }, + { + "dialect": "postgres", + "sql": "SELECT CAST(e'\\176' AS BYTEA)", + "formatted": "SELECT CAST(e'\\176' AS BYTEA)" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM x WHERE SUBSTRING('Thomas' FROM '...$') IN ('mas')", + "formatted": "SELECT *\nFROM x\nWHERE SUBSTRING('Thomas' FROM '...$') IN ('mas')" + }, + { + "dialect": "postgres", + "sql": "SELECT TRIM(' X' FROM ' XXX ')", + "formatted": "SELECT TRIM(' X' FROM ' XXX ')" + }, + { + "dialect": "postgres", + "sql": "SELECT TRIM(LEADING 'bla' FROM ' XXX ' COLLATE utf8_bin)", + "formatted": "SELECT TRIM(LEADING 'bla' FROM ' XXX ' COLLATE utf8_bin)" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM JSON_TO_RECORDSET(z) AS y(\"rank\" INT)", + "formatted": "SELECT *\nFROM JSON_TO_RECORDSET(z) AS y(\"rank\" INT)" + }, + { + "dialect": "postgres", + "sql": "SELECT ~x", + "formatted": "SELECT ~x" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM r CROSS JOIN LATERAL UNNEST(ARRAY[1]) AS s(location)", + "formatted": "SELECT *\nFROM r\nCROSS JOIN LATERAL UNNEST(ARRAY[1]) AS s(location)" + }, + { + "dialect": "postgres", + "sql": "SELECT CURRENT_SCHEMA", + "formatted": "SELECT CURRENT_SCHEMA" + }, + { + "dialect": "postgres", + "sql": "SELECT CURRENT_USER", + "formatted": "SELECT CURRENT_USER" + }, + { + "dialect": "postgres", + "sql": "SELECT CURRENT_ROLE", + "formatted": "SELECT CURRENT_ROLE" + }, + { + "dialect": "postgres", + "sql": "SELECT VERSION()", + "formatted": "SELECT VERSION()" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM ONLY t1", + "formatted": "SELECT *\nFROM ONLY t1" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '-1 MONTH'", + "formatted": "SELECT INTERVAL '-1 MONTH'" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '4.1 DAY'", + "formatted": "SELECT INTERVAL '4.1 DAY'" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '3.14159 HOUR'", + "formatted": "SELECT INTERVAL '3.14159 HOUR'" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '2.5 MONTH'", + "formatted": "SELECT INTERVAL '2.5 MONTH'" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '-10.75 MINUTE'", + "formatted": "SELECT INTERVAL '-10.75 MINUTE'" + }, + { + "dialect": "postgres", + "sql": "SELECT INTERVAL '0.123456789 SECOND'", + "formatted": "SELECT INTERVAL '0.123456789 SECOND'" + }, + { + "dialect": "postgres", + "sql": "SELECT date_col - INTERVAL '30' FROM t", + "formatted": "SELECT date_col - INTERVAL '30'\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT date_col - INTERVAL '1' AS one_second_later", + "formatted": "SELECT date_col - INTERVAL '1' AS one_second_later" + }, + { + "dialect": "postgres", + "sql": "SELECT date_col - INTERVAL '30' DAY FROM t", + "formatted": null + }, + { + "dialect": "postgres", + "sql": "SELECT date_col - INTERVAL '1' HOUR AS one_hour_later", + "formatted": null + }, + { + "dialect": "postgres", + "sql": "SELECT SUM(x) OVER (PARTITION BY y ORDER BY interval ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - SUM(x) OVER (PARTITION BY y ORDER BY interval ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS total", + "formatted": "SELECT SUM(x) OVER (PARTITION BY y ORDER BY interval ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) - SUM(x) OVER (PARTITION BY y ORDER BY interval ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS total" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM test_data, LATERAL JSONB_ARRAY_ELEMENTS(data) WITH ORDINALITY AS elem(value, ordinality)", + "formatted": "SELECT *\nFROM test_data, LATERAL JSONB_ARRAY_ELEMENTS(data) WITH ORDINALITY AS elem(value, ordinality)" + }, + { + "dialect": "postgres", + "sql": "SELECT id, name FROM xml_data AS t, XMLTABLE('/root/user' PASSING t.xml COLUMNS id INT PATH '@id', name TEXT PATH 'name/text()') AS x", + "formatted": "SELECT\n id,\n name\nFROM xml_data AS t, XMLTABLE(\n '/root/user'\n PASSING\n t.xml\n COLUMNS\n id INT PATH '@id',\n name TEXT PATH 'name/text()'\n) AS x" + }, + { + "dialect": "postgres", + "sql": "SELECT id, value FROM xml_content AS t, XMLTABLE(XMLNAMESPACES('http://example.com/ns1' AS ns1, 'http://example.com/ns2' AS ns2), '/root/data' PASSING t.xml COLUMNS id INT PATH '@ns1:id', value TEXT PATH 'ns2:value/text()') AS x", + "formatted": null + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM t WHERE some_column >= CURRENT_DATE + INTERVAL '1 day 1 hour' AND some_another_column IS TRUE", + "formatted": "SELECT *\nFROM t\nWHERE\n some_column >= CURRENT_DATE + INTERVAL '1 day 1 hour' AND some_another_column IS TRUE" + }, + { + "dialect": "postgres", + "sql": "WITH t1 AS MATERIALIZED (SELECT 1), t2 AS NOT MATERIALIZED (SELECT 2) SELECT * FROM t1, t2", + "formatted": "WITH t1 AS MATERIALIZED (\n SELECT 1\n),\n\nt2 AS NOT MATERIALIZED (\n SELECT 2\n)\n\nSELECT *\nFROM t1, t2" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY", + "formatted": "SELECT *\nFROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY AS kv_json", + "formatted": "SELECT *\nFROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY AS kv_json" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY AS kv_json(a, b)", + "formatted": "SELECT *\nFROM JSON_ARRAY_ELEMENTS('[1,true, [2,false]]') WITH ORDINALITY AS kv_json(a, b)" + }, + { + "dialect": "postgres", + "sql": "SELECT SUM(x) OVER a, SUM(y) OVER b FROM c WINDOW a AS (PARTITION BY d), b AS (PARTITION BY e)", + "formatted": "SELECT\n SUM(x) OVER a,\n SUM(y) OVER b\nFROM c\nWINDOW\n a AS (PARTITION BY d),\n b AS (PARTITION BY e)" + }, + { + "dialect": "postgres", + "sql": "SELECT CASE WHEN SUBSTRING('abcdefg' FROM 1) IN ('ab') THEN 1 ELSE 0 END", + "formatted": "SELECT\n CASE\n WHEN SUBSTRING('abcdefg' FROM 1) IN ('ab')\n THEN 1\n ELSE 0\n END" + }, + { + "dialect": "postgres", + "sql": "SELECT CASE WHEN SUBSTRING('abcdefg' FROM 1 FOR 2) IN ('ab') THEN 1 ELSE 0 END", + "formatted": "SELECT\n CASE\n WHEN SUBSTRING('abcdefg' FROM 1 FOR 2) IN ('ab')\n THEN 1\n ELSE 0\n END" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM \"x\" WHERE SUBSTRING(\"x\".\"foo\" FROM 1 FOR 2) IN ('mas')", + "formatted": "SELECT *\nFROM \"x\"\nWHERE SUBSTRING(\"x\".\"foo\" FROM 1 FOR 2) IN ('mas')" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM x WHERE SUBSTRING('Thomas' FROM '%#\"o_a#\"_' FOR '#') IN ('mas')", + "formatted": "SELECT *\nFROM x\nWHERE SUBSTRING('Thomas' FROM '%#\"o_a#\"_' FOR '#') IN ('mas')" + }, + { + "dialect": "postgres", + "sql": "SELECT SUBSTRING('bla' + 'foo' || 'bar' FROM 3 - 1 + 5 FOR 4 + SOME_FUNC(arg1, arg2))", + "formatted": "SELECT SUBSTRING('bla' + 'foo' || 'bar' FROM 3 - 1 + 5 FOR 4 + SOME_FUNC(arg1, arg2))" + }, + { + "dialect": "postgres", + "sql": "SELECT TO_TIMESTAMP(1284352323.5), TO_TIMESTAMP('05 Dec 2000', 'DD Mon YYYY')", + "formatted": "SELECT\n TO_TIMESTAMP(1284352323.5),\n TO_TIMESTAMP('05 Dec 2000', 'DD Mon YYYY')" + }, + { + "dialect": "postgres", + "sql": "SELECT TO_TIMESTAMP('05 Dec 2000 10:00 AM', 'DD Mon YYYY HH:MI AM')", + "formatted": "SELECT TO_TIMESTAMP('05 Dec 2000 10:00 AM', 'DD Mon YYYY HH:MI AM')" + }, + { + "dialect": "postgres", + "sql": "SELECT TO_TIMESTAMP('05 Dec 2000 10:00 PM', 'DD Mon YYYY HH:MI PM')", + "formatted": "SELECT TO_TIMESTAMP('05 Dec 2000 10:00 PM', 'DD Mon YYYY HH:MI PM')" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM foo, LATERAL (SELECT * FROM bar WHERE bar.id = foo.bar_id) AS ss", + "formatted": "SELECT *\nFROM foo, LATERAL (\n SELECT *\n FROM bar\n WHERE bar.id = foo.bar_id\n) AS ss" + }, + { + "dialect": "postgres", + "sql": "SELECT c.oid, n.nspname, c.relname FROM pg_catalog.pg_class AS c LEFT JOIN pg_catalog.pg_namespace AS n ON n.oid = c.relnamespace WHERE c.relname OPERATOR(pg_catalog.~) '^(courses)$' COLLATE pg_catalog.default AND pg_catalog.PG_TABLE_IS_VISIBLE(c.oid) ORDER BY 2, 3", + "formatted": "SELECT\n c.oid,\n n.nspname,\n c.relname\nFROM pg_catalog.pg_class AS c\nLEFT JOIN pg_catalog.pg_namespace AS n\n ON n.oid = c.relnamespace\nWHERE\n c.relname OPERATOR(pg_catalog.~) '^(courses)$' COLLATE pg_catalog.default AND pg_catalog.PG_TABLE_IS_VISIBLE(c.oid)\nORDER BY\n 2,\n 3" + }, + { + "dialect": "postgres", + "sql": "SELECT date_add(current_date, interval '7' day)", + "formatted": null + }, + { + "dialect": "postgres", + "sql": "SELECT e'foo \\' bar'", + "formatted": "SELECT e'foo \\' bar'" + }, + { + "dialect": "postgres", + "sql": "SELECT e'\\n'", + "formatted": "SELECT e'\\n'" + }, + { + "dialect": "postgres", + "sql": "SELECT e'\\t'", + "formatted": "SELECT e'\\t'" + }, + { + "dialect": "postgres", + "sql": "SELECT e'update table_name set a = \\'foo\\' where 1 = 0' AS x FROM tab", + "formatted": "SELECT e'update table_name set a = \\'foo\\' where 1 = 0' AS x\nFROM tab" + }, + { + "dialect": "postgres", + "sql": "select count() OVER(partition by a order by a range offset preceding exclude current row)", + "formatted": "SELECT COUNT() OVER (PARTITION BY a ORDER BY a range offset preceding exclude CURRENT ROW)" + }, + { + "dialect": "postgres", + "sql": "SELECT SUBSTRING('Thomas' FOR 3 FROM 2)", + "formatted": "SELECT SUBSTRING('Thomas' FOR 3 FROM 2)" + }, + { + "dialect": "postgres", + "sql": "SELECT DATE_PART('isodow'::varchar(6), current_date)", + "formatted": "SELECT DATE_PART('isodow'::varchar(6), CURRENT_DATE)" + }, + { + "dialect": "postgres", + "sql": "SELECT $$a$$", + "formatted": "SELECT $$a$$" + }, + { + "dialect": "postgres", + "sql": "SELECT $$Dianne's order$$", + "formatted": "SELECT $$Dianne's order$$" + }, + { + "dialect": "postgres", + "sql": "SELECT $$The price is $9.95$$ AS msg", + "formatted": "SELECT $$The price is $9.95$$ AS msg" + }, + { + "dialect": "postgres", + "sql": "SELECT 'testa 1' NOT LIKE ALL (ARRAY['testa%', 'testb%'])", + "formatted": "SELECT\n 'testa 1' NOT LIKE ALL (\n ARRAY['testa%', 'testb%']\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT 'testa 1' NOT LIKE ANY(ARRAY['testa%', 'testb%'])", + "formatted": "SELECT\n 'testa 1' NOT LIKE ANY(\n ARRAY['testa%', 'testb%']\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT 'testa 1' NOT ILIKE ALL (ARRAY['testa%', 'testb%'])", + "formatted": "SELECT\n 'testa 1' NOT ILIKE ALL (\n ARRAY['testa%', 'testb%']\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT 'testa 1' NOT ILIKE ANY(ARRAY['testa%', 'testb%'])", + "formatted": "SELECT\n 'testa 1' NOT ILIKE ANY(\n ARRAY['testa%', 'testb%']\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT NOT 'testa 1' LIKE ALL (ARRAY['testa%', 'testb%'])", + "formatted": "SELECT\n NOT 'testa 1' LIKE ALL (\n ARRAY['testa%', 'testb%']\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT NOT ('testa 1' LIKE ALL (ARRAY['testa%', 'testb%']))", + "formatted": "SELECT\n NOT (\n 'testa 1' LIKE ALL (\n ARRAY['testa%', 'testb%']\n )\n )" + }, + { + "dialect": "postgres", + "sql": "SELECT to_timestamp(123)::time without time zone", + "formatted": "SELECT to_timestamp(123)::time without time zone" + }, + { + "dialect": "postgres", + "sql": "SELECT SUM(x) OVER (PARTITION BY a ORDER BY d ROWS 1 PRECEDING)", + "formatted": "SELECT SUM(x) OVER (PARTITION BY a ORDER BY d ROWS 1 PRECEDING)" + }, + { + "dialect": "postgres", + "sql": "SELECT SUBSTRING(2022::CHAR(4) || LPAD(3::CHAR(2), 2, '0') FROM 3 FOR 4)", + "formatted": "SELECT SUBSTRING(2022::CHAR(4) || LPAD(3::CHAR(2), 2, '0') FROM 3 FOR 4)" + }, + { + "dialect": "postgres", + "sql": "SELECT m.name FROM manufacturers AS m LEFT JOIN LATERAL GET_PRODUCT_NAMES(m.id) pname ON TRUE WHERE pname IS NULL", + "formatted": "SELECT m.name\nFROM manufacturers AS m\nLEFT JOIN LATERAL GET_PRODUCT_NAMES(m.id) pname\n ON TRUE\nWHERE pname IS NULL", + "previously_formatted": "SELECT\n m.name\nFROM manufacturers AS m\nLEFT JOIN LATERAL GET_PRODUCT_NAMES(m.id) AS pname\n ON TRUE\nWHERE\n pname IS NULL" + }, + { + "dialect": "postgres", + "sql": "SELECT p1.id, p2.id, v1, v2 FROM polygons AS p1, polygons AS p2, LATERAL VERTICES(p1.poly) v1, LATERAL VERTICES(p2.poly) v2 WHERE (v1 <-> v2) < 10 AND p1.id <> p2.id", + "formatted": "SELECT\n p1.id,\n p2.id,\n v1,\n v2\nFROM polygons AS p1, polygons AS p2, LATERAL VERTICES(p1.poly) v1, LATERAL VERTICES(p2.poly) v2\nWHERE\n (\n v1 <-> v2\n ) < 10 AND p1.id <> p2.id", + "previously_formatted": "SELECT\n p1.id,\n p2.id,\n v1,\n v2\nFROM polygons AS p1, polygons AS p2, LATERAL VERTICES(p1.poly) AS v1, LATERAL VERTICES(p2.poly) AS v2\nWHERE\n (\n v1 <-> v2\n ) < 10 AND p1.id <> p2.id" + }, + { + "dialect": "postgres", + "sql": "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE deleted NOTNULL", + "formatted": "SELECT\n id,\n email,\n CAST(deleted AS TEXT)\nFROM users\nWHERE deleted NOTNULL" + }, + { + "dialect": "postgres", + "sql": "SELECT id, email, CAST(deleted AS TEXT) FROM users WHERE NOT deleted ISNULL", + "formatted": "SELECT\n id,\n email,\n CAST(deleted AS TEXT)\nFROM users\nWHERE NOT deleted ISNULL" + }, + { + "dialect": "postgres", + "sql": "SELECT JSON_ARRAY_ELEMENTS((foo->'sections')::JSON) AS sections", + "formatted": "SELECT\n JSON_ARRAY_ELEMENTS((\n foo -> 'sections'\n )::JSON) AS sections" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM t1*", + "formatted": "SELECT *\nFROM t1*" + }, + { + "dialect": "postgres", + "sql": "SELECT SUBSTRING('afafa' for 1)", + "formatted": "SELECT SUBSTRING('afafa' FOR 1)" + }, + { + "dialect": "postgres", + "sql": "\n WITH\n json_data AS (SELECT '{\"field_id\": [1, 2, 3]}'::JSON AS data),\n field_ids AS (SELECT 'field_id' AS field_id)\n\n SELECT\n JSON_ARRAY_ELEMENTS(json_data.data -> field_ids.field_id) AS element\n FROM json_data, field_ids\n ", + "formatted": "WITH json_data AS (\n SELECT '{\"field_id\": [1, 2, 3]}'::JSON AS data\n),\n\nfield_ids AS (\n SELECT 'field_id' AS field_id\n)\n\nSELECT JSON_ARRAY_ELEMENTS(json_data.data -> field_ids.field_id) AS element\nFROM json_data, field_ids" + }, + { + "dialect": "postgres", + "sql": "SELECT TRIM(LEADING ' XXX ' COLLATE \"de_DE\")", + "formatted": "SELECT TRIM(LEADING ' XXX ' COLLATE \"de_DE\")" + }, + { + "dialect": "postgres", + "sql": "SELECT TRIM(TRAILING ' XXX ' COLLATE \"de_DE\")", + "formatted": "SELECT TRIM(TRAILING ' XXX ' COLLATE \"de_DE\")" + }, + { + "dialect": "postgres", + "sql": "SELECT js, js IS JSON AS \"json?\", js IS JSON VALUE AS \"scalar?\", js IS JSON SCALAR AS \"scalar?\", js IS JSON OBJECT AS \"object?\", js IS JSON ARRAY AS \"array?\" FROM t", + "formatted": "SELECT\n js,\n js IS JSON AS \"json?\",\n js IS JSON VALUE AS \"scalar?\",\n js IS JSON SCALAR AS \"scalar?\",\n js IS JSON OBJECT AS \"object?\",\n js IS JSON ARRAY AS \"array?\"\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT js, js IS JSON ARRAY WITH UNIQUE KEYS AS \"array w. UK?\", js IS JSON ARRAY WITHOUT UNIQUE KEYS AS \"array w/o UK?\", js IS JSON ARRAY UNIQUE KEYS AS \"array w UK 2?\" FROM t", + "formatted": "SELECT\n js,\n js IS JSON ARRAY WITH UNIQUE KEYS AS \"array w. UK?\",\n js IS JSON ARRAY WITHOUT UNIQUE KEYS AS \"array w/o UK?\",\n js IS JSON ARRAY UNIQUE KEYS AS \"array w UK 2?\"\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT 1 FROM ((VALUES (1)) AS vals(id) LEFT OUTER JOIN tbl ON vals.id = tbl.id)", + "formatted": "SELECT 1\nFROM ((VALUES (1)) AS vals(id)\nLEFT OUTER JOIN tbl\n ON vals.id = tbl.id)" + }, + { + "dialect": "postgres", + "sql": "SELECT OVERLAY(a PLACING b FROM 1)", + "formatted": "SELECT OVERLAY(a PLACING b FROM 1)" + }, + { + "dialect": "postgres", + "sql": "SELECT OVERLAY(a PLACING b FROM 1 FOR 1)", + "formatted": "SELECT OVERLAY(a PLACING b FROM 1 FOR 1)" + }, + { + "dialect": "postgres", + "sql": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY a) FILTER(WHERE CAST(b AS BOOLEAN)) AS mean_value FROM (VALUES (0, 't')) AS fake_data(a, b)", + "formatted": "SELECT PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY a) FILTER(WHERE CAST(b AS BOOLEAN)) AS mean_value\nFROM (VALUES (0, 't')) AS fake_data(a, b)" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM foo WHERE id = %s", + "formatted": "SELECT *\nFROM foo\nWHERE id = %s" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM foo WHERE id = %(id_param)s", + "formatted": "SELECT *\nFROM foo\nWHERE id = %(id_param)s" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM foo WHERE id = ?", + "formatted": "SELECT *\nFROM foo\nWHERE id = ?" + }, + { + "dialect": "postgres", + "sql": "SELECT '{\"a\":1, \"b\":2, \"c\":3}'::jsonb ?| array['b', 'c']", + "formatted": "SELECT '{\"a\":1, \"b\":2, \"c\":3}'::jsonb ?| ARRAY['b', 'c']" + }, + { + "dialect": "postgres", + "sql": "SELECT '[\"a\", \"b\"]'::jsonb ?& array['a', 'b']", + "formatted": "SELECT '[\"a\", \"b\"]'::jsonb ?& ARRAY['a', 'b']" + }, + { + "dialect": "postgres", + "sql": "SELECT '[\"a\", {\"b\":1}]'::jsonb #- '{1,b}'", + "formatted": "SELECT '[\"a\", {\"b\":1}]'::jsonb #- '{1,b}'" + }, + { + "dialect": "postgres", + "sql": "SELECT JSON_AGG(DISTINCT name) FROM users", + "formatted": "SELECT JSON_AGG(DISTINCT name)\nFROM users" + }, + { + "dialect": "postgres", + "sql": "SELECT JSON_AGG(c1 ORDER BY c1) FROM (VALUES ('c'), ('b'), ('a')) AS t(c1)", + "formatted": "SELECT JSON_AGG(c1 ORDER BY c1)\nFROM (VALUES ('c'), ('b'), ('a')) AS t(c1)" + }, + { + "dialect": "postgres", + "sql": "SELECT JSON_AGG(DISTINCT c1 ORDER BY c1) FROM (VALUES ('c'), ('b'), ('a')) AS t(c1)", + "formatted": "SELECT JSON_AGG(DISTINCT c1 ORDER BY c1)\nFROM (VALUES ('c'), ('b'), ('a')) AS t(c1)" + }, + { + "dialect": "postgres", + "sql": "SELECT NUMRANGE(1.1, 2.2) -|- NUMRANGE(2.2, 3.3)", + "formatted": "SELECT NUMRANGE(1.1, 2.2) -|- NUMRANGE(2.2, 3.3)" + }, + { + "dialect": "postgres", + "sql": "SELECT SLOPE(point '(4,4)', point '(0,0)')", + "formatted": "SELECT SLOPE(point '(4,4)', point '(0,0)')" + }, + { + "dialect": "postgres", + "sql": "SELECT MLEAST(VARIADIC ARRAY[10, -1, 5, 4.4])", + "formatted": "SELECT MLEAST(VARIADIC ARRAY[10, -1, 5, 4.4])" + }, + { + "dialect": "postgres", + "sql": "SELECT MLEAST(VARIADIC ARRAY[]::numeric[])", + "formatted": null + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM schema_name.table_name st WHERE JSON_EXTRACT_PATH_TEXT((st.data)::json, variadic array['test'::text]) = 'test'::text", + "formatted": "SELECT *\nFROM schema_name.table_name st\nWHERE\n JSON_EXTRACT_PATH_TEXT(\n (\n st.data\n )::json,\n VARIADIC ARRAY['test'::text]\n ) = 'test'::text", + "previously_formatted": "SELECT\n *\nFROM schema_name.table_name AS st\nWHERE\n JSON_EXTRACT_PATH_TEXT(\n (\n st.data\n )::json,\n VARIADIC ARRAY['test'::text]\n ) = 'test'::text" + }, + { + "dialect": "postgres", + "sql": "WITH t(c) AS (SELECT 1) SELECT * INTO UNLOGGED foo FROM (SELECT c AS c FROM t) AS temp", + "formatted": "WITH t(c) AS (\n SELECT 1\n)\n\nSELECT *\nINTO UNLOGGED foo\nFROM (\n SELECT c AS c\n FROM t\n) AS temp" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM UNNEST(ARRAY[1, 2], ARRAY['foo', 'bar', 'baz']) AS x(a, b)", + "formatted": "SELECT *\nFROM UNNEST(ARRAY[1, 2], ARRAY['foo', 'bar', 'baz']) AS x(a, b)" + }, + { + "dialect": "postgres", + "sql": "SELECT TRIM(ARRAY_TO_STRING(ARRAY(SELECT val FROM UNNEST(ARRAY['a', 'b']) WITH ORDINALITY AS u(val, rn)), ' '))", + "formatted": "SELECT\n TRIM(ARRAY_TO_STRING(\n ARRAY(\n SELECT val\n FROM UNNEST(ARRAY['a', 'b']) WITH ORDINALITY AS u(val, rn)\n ),\n ' '\n ))" + }, + { + "dialect": "postgres", + "sql": "SELECT a, LOGICAL_OR(b) FROM table GROUP BY a", + "formatted": "SELECT\n a,\n LOGICAL_OR(b)\nFROM table\nGROUP BY\n a" + }, + { + "dialect": "postgres", + "sql": "SELECT CONCAT('abcde', 2, NULL, 22)", + "formatted": "SELECT CONCAT('abcde', 2, NULL, 22)" + }, + { + "dialect": "postgres", + "sql": "SELECT u & 5 FROM t", + "formatted": "SELECT u & 5\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT (U&'\\FE01' || 'Test literal') AS label FROM data", + "formatted": "SELECT\n (\n U&'\\FE01' || 'Test literal'\n ) AS label\nFROM data" + }, + { + "dialect": "postgres", + "sql": "SELECT U&'d!0061t!+000061' UESCAPE '!' AS label", + "formatted": "SELECT U&'d!0061t!+000061' UESCAPE '!' AS label" + }, + { + "dialect": "postgres", + "sql": "SELECT u&'\\0441\\043B\\043E\\043D'", + "formatted": "SELECT u&'\\0441\\043B\\043E\\043D'" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM ROWS FROM (FUNC1(col1, col2))", + "formatted": "SELECT *\nFROM ROWS FROM (FUNC1(col1, col2))" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM ROWS FROM (FUNC1(col1) AS alias1(\"col1\" TEXT), FUNC2(col2) AS alias2(\"col2\" INT)) WITH ORDINALITY", + "formatted": "SELECT *\nFROM ROWS FROM (FUNC1(col1) AS alias1(\"col1\" TEXT), FUNC2(col2) AS alias2(\"col2\" INT)) WITH ORDINALITY" + }, + { + "dialect": "postgres", + "sql": "SELECT * FROM table1, ROWS FROM (FUNC1(col1) AS alias1(\"col1\" TEXT)) WITH ORDINALITY AS alias3(\"col3\" INT, \"col4\" TEXT)", + "formatted": "SELECT *\nFROM table1, ROWS FROM (FUNC1(col1) AS alias1(\"col1\" TEXT)) WITH ORDINALITY AS alias3(\"col3\" INT, \"col4\" TEXT)" + }, + { + "dialect": "postgres", + "sql": "SELECT ARRAY_LENGTH(ARRAY[1, 2, 3], 1)", + "formatted": "SELECT ARRAY_LENGTH(ARRAY[1, 2, 3], 1)" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME foo)", + "formatted": "SELECT XMLELEMENT(NAME foo)" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar))", + "formatted": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar))" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME test, XMLATTRIBUTES(a, b)) FROM test", + "formatted": "SELECT XMLELEMENT(NAME test, XMLATTRIBUTES(a, b))\nFROM test" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES(CURRENT_DATE AS bar), 'cont', 'ent')", + "formatted": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES(CURRENT_DATE AS bar), 'cont', 'ent')" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME \"foo$bar\", XMLATTRIBUTES('xyz' AS \"a&b\"))", + "formatted": "SELECT XMLELEMENT(NAME \"foo$bar\", XMLATTRIBUTES('xyz' AS \"a&b\"))" + }, + { + "dialect": "postgres", + "sql": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar), XMLELEMENT(NAME abc), XMLCOMMENT('test'), XMLELEMENT(NAME xyz))", + "formatted": "SELECT XMLELEMENT(NAME foo, XMLATTRIBUTES('xyz' AS bar), XMLELEMENT(NAME abc), XMLCOMMENT('test'), XMLELEMENT(NAME xyz))" + }, + { + "dialect": "postgres", + "sql": "WITH RECURSIVE search_graph(id, link, data, depth) AS (SELECT g.id, g.link, g.data, 1 FROM graph AS g UNION ALL SELECT g.id, g.link, g.data, sg.depth + 1 FROM graph AS g, search_graph AS sg WHERE g.id = sg.link) CYCLE id SET is_cycle USING path SELECT * FROM search_graph", + "formatted": "WITH RECURSIVE search_graph(id, link, data, depth) AS (\n SELECT\n g.id,\n g.link,\n g.data,\n 1\n FROM graph AS g\n UNION ALL\n SELECT\n g.id,\n g.link,\n g.data,\n sg.depth + 1\n FROM graph AS g, search_graph AS sg\n WHERE g.id = sg.link\n)\n\nCYCLE id SET is_cycle USING path\nSELECT *\nFROM search_graph" + }, + { + "dialect": "postgres", + "sql": "SELECT a <<->> b", + "formatted": "SELECT a <<->> b" + }, + { + "dialect": "postgres", + "sql": "SELECT r IS NOT NULL FROM t", + "formatted": "SELECT r IS NOT NULL\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT NOT r IS NULL FROM t", + "formatted": "SELECT NOT r IS NULL\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT NOT r IS NOT NULL FROM t", + "formatted": "SELECT NOT r IS NOT NULL\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT r NOTNULL FROM t", + "formatted": "SELECT r NOTNULL\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT r ISNULL FROM t", + "formatted": "SELECT r ISNULL\nFROM t" + }, + { + "dialect": "postgres", + "sql": "SELECT ARRAY[1, 2, 3] <@ ARRAY[1, 2]", + "formatted": "SELECT ARRAY[1, 2, 3] <@ ARRAY[1, 2]" + }, + { + "dialect": "snowflake", + "sql": "WITH t AS (SELECT PARSE_JSON('{\"level1\": {\"level2\": {\"level3\": \"value\"}}}') AS data) SELECT data: level1 : level2 : level3::VARIANT FROM t", + "formatted": "WITH t AS (\n SELECT PARSE_JSON('{\"level1\": {\"level2\": {\"level3\": \"value\"}}}') AS data\n)\n\nSELECT data:level1:level2:level3::VARIANT\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM x ASOF JOIN y OFFSET MATCH_CONDITION (x.a > y.a)", + "formatted": "SELECT *\nFROM x\nASOF JOIN y OFFSET MATCH_CONDITION (x.a > y.a)", + "previously_formatted": "SELECT\n *\nFROM x\nASOF JOIN y AS OFFSET MATCH_CONDITION (x.a > y.a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM x ASOF JOIN y LIMIT MATCH_CONDITION (x.a > y.a)", + "formatted": "SELECT *\nFROM x\nASOF JOIN y LIMIT MATCH_CONDITION (x.a > y.a)", + "previously_formatted": "SELECT\n *\nFROM x\nASOF JOIN y AS LIMIT MATCH_CONDITION (x.a > y.a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT session", + "formatted": "SELECT session" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_PART(EPOCH_MILLISECOND, CURRENT_TIMESTAMP()) AS a", + "formatted": "SELECT DATE_PART(EPOCH_MILLISECOND, CURRENT_TIMESTAMP()) AS a" + }, + { + "dialect": "snowflake", + "sql": "SELECT GET(a, b)", + "formatted": "SELECT GET(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HASH_AGG(a, b, c, d)", + "formatted": "SELECT HASH_AGG(a, b, c, d)" + }, + { + "dialect": "snowflake", + "sql": "SELECT GREATEST(1, 2, 3, NULL)", + "formatted": "SELECT GREATEST(1, 2, 3, NULL)" + }, + { + "dialect": "snowflake", + "sql": "SELECT GREATEST_IGNORE_NULLS(1, 2, 3, NULL)", + "formatted": "SELECT GREATEST_IGNORE_NULLS(1, 2, 3, NULL)" + }, + { + "dialect": "snowflake", + "sql": "SELECT LEAST(5, NULL, 7, 3)", + "formatted": "SELECT LEAST(5, NULL, 7, 3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT LEAST_IGNORE_NULLS(5, NULL, 7, 3)", + "formatted": "SELECT LEAST_IGNORE_NULLS(5, NULL, 7, 3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAX(x)", + "formatted": "SELECT MAX(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT COUNT(x)", + "formatted": "SELECT COUNT(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MIN(amount)", + "formatted": "SELECT MIN(amount)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MODE(x)", + "formatted": "SELECT MODE(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MODE(status) OVER (PARTITION BY region) FROM orders", + "formatted": "SELECT MODE(status) OVER (PARTITION BY region)\nFROM orders" + }, + { + "dialect": "snowflake", + "sql": "SELECT TAN(x)", + "formatted": "SELECT TAN(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT COS(x)", + "formatted": "SELECT COS(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT SINH(1.5)", + "formatted": "SELECT SINH(1.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MOD(x, y)", + "formatted": "SELECT MOD(x, y)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ROUND(x)", + "formatted": "SELECT ROUND(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ROUND(123.456, -1)", + "formatted": "SELECT ROUND(123.456, -1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ROUND(123.456, 2, 'HALF_AWAY_FROM_ZERO')", + "formatted": "SELECT ROUND(123.456, 2, 'HALF_AWAY_FROM_ZERO')" + }, + { + "dialect": "snowflake", + "sql": "SELECT FLOOR(x)", + "formatted": "SELECT FLOOR(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT FLOOR(135.135, 1)", + "formatted": "SELECT FLOOR(135.135, 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT FLOOR(x, -1)", + "formatted": "SELECT FLOOR(x, -1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY salary) FROM employees", + "formatted": "SELECT PERCENTILE_CONT(0.25) WITHIN GROUP (ORDER BY salary)\nFROM employees" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K(col) FROM t", + "formatted": "SELECT APPROX_TOP_K(col)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K(category, 3) FROM t", + "formatted": "SELECT APPROX_TOP_K(category, 3)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT MINHASH(5, col)", + "formatted": "SELECT MINHASH(5, col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MINHASH(5, col1, col2)", + "formatted": "SELECT MINHASH(5, col1, col2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MINHASH(5, *)", + "formatted": "SELECT MINHASH(5, *)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MINHASH_COMBINE(minhash_col)", + "formatted": "SELECT MINHASH_COMBINE(minhash_col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROXIMATE_SIMILARITY(minhash_col)", + "formatted": "SELECT APPROXIMATE_SIMILARITY(minhash_col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROXIMATE_JACCARD_INDEX(minhash_col)", + "formatted": "SELECT APPROXIMATE_JACCARD_INDEX(minhash_col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_PERCENTILE_ACCUMULATE(col)", + "formatted": "SELECT APPROX_PERCENTILE_ACCUMULATE(col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_PERCENTILE_ESTIMATE(state, 0.5)", + "formatted": "SELECT APPROX_PERCENTILE_ESTIMATE(state, 0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K_ACCUMULATE(col, 10)", + "formatted": "SELECT APPROX_TOP_K_ACCUMULATE(col, 10)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K_COMBINE(state, 2)", + "formatted": "SELECT APPROX_TOP_K_COMBINE(state, 2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K_COMBINE(state)", + "formatted": "SELECT APPROX_TOP_K_COMBINE(state)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K_ESTIMATE(state_column, 4)", + "formatted": "SELECT APPROX_TOP_K_ESTIMATE(state_column, 4)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_TOP_K_ESTIMATE(state_column)", + "formatted": "SELECT APPROX_TOP_K_ESTIMATE(state_column)" + }, + { + "dialect": "snowflake", + "sql": "SELECT APPROX_PERCENTILE_COMBINE(state_column)", + "formatted": "SELECT APPROX_PERCENTILE_COMBINE(state_column)" + }, + { + "dialect": "snowflake", + "sql": "SELECT EQUAL_NULL(1, 2)", + "formatted": "SELECT EQUAL_NULL(1, 2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT FACTORIAL(5)", + "formatted": "SELECT FACTORIAL(5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_LENGTH('abc')", + "formatted": "SELECT BIT_LENGTH('abc')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_LENGTH(x'A1B2')", + "formatted": "SELECT BIT_LENGTH(x'A1B2')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITMAP_BUCKET_NUMBER(32769)", + "formatted": "SELECT BITMAP_BUCKET_NUMBER(32769)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITMAP_CONSTRUCT_AGG(value)", + "formatted": "SELECT BITMAP_CONSTRUCT_AGG(value)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITMAP_COUNT(BITMAP_CONSTRUCT_AGG(value)) FROM TABLE(FLATTEN(INPUT => ARRAY_CONSTRUCT(1, 2, 3, 5)))", + "formatted": "SELECT BITMAP_COUNT(BITMAP_CONSTRUCT_AGG(value))\nFROM TABLE(FLATTEN(INPUT => ARRAY_CONSTRUCT(1, 2, 3, 5)))" + }, + { + "dialect": "snowflake", + "sql": "SELECT BOOLAND(1, -2)", + "formatted": "SELECT BOOLAND(1, -2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BOOLXOR(2, 0)", + "formatted": "SELECT BOOLXOR(2, 0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BOOLOR(1, 0)", + "formatted": "SELECT BOOLOR(1, 0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_BOOLEAN('true')", + "formatted": "SELECT TO_BOOLEAN('true')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_BOOLEAN(1)", + "formatted": "SELECT TO_BOOLEAN(1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_VARIANT(123)", + "formatted": "SELECT TO_VARIANT(123)" + }, + { + "dialect": "snowflake", + "sql": "SELECT IS_NULL_VALUE(GET_PATH(payload, 'field'))", + "formatted": "SELECT IS_NULL_VALUE(GET_PATH(payload, 'field'))" + }, + { + "dialect": "snowflake", + "sql": "SELECT RTRIMMED_LENGTH(' ABCD ')", + "formatted": "SELECT RTRIMMED_LENGTH(' ABCD ')" + }, + { + "dialect": "snowflake", + "sql": "SELECT HEX_DECODE_STRING('48656C6C6F')", + "formatted": "SELECT HEX_DECODE_STRING('48656C6C6F')" + }, + { + "dialect": "snowflake", + "sql": "SELECT HEX_ENCODE('Hello World')", + "formatted": "SELECT HEX_ENCODE('Hello World')" + }, + { + "dialect": "snowflake", + "sql": "SELECT HEX_ENCODE('Hello World', 1)", + "formatted": "SELECT HEX_ENCODE('Hello World', 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HEX_ENCODE('Hello World', 0)", + "formatted": "SELECT HEX_ENCODE('Hello World', 0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT IFNULL(col1, col2)", + "formatted": "SELECT IFNULL(col1, col2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT NEXT_DAY('2025-10-15', 'FRIDAY')", + "formatted": "SELECT NEXT_DAY('2025-10-15', 'FRIDAY')" + }, + { + "dialect": "snowflake", + "sql": "SELECT NVL2(col1, col2, col3)", + "formatted": "SELECT NVL2(col1, col2, col3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT NVL(col1, col2)", + "formatted": "SELECT NVL(col1, col2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CHR(8364)", + "formatted": "SELECT CHR(8364)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CHECK_JSON('{\"key\": \"value\"}')", + "formatted": "SELECT CHECK_JSON('{\"key\": \"value\"}')" + }, + { + "dialect": "snowflake", + "sql": "SELECT CHECK_XML('value')", + "formatted": "SELECT CHECK_XML('value')" + }, + { + "dialect": "snowflake", + "sql": "SELECT CHECK_XML('value', TRUE)", + "formatted": "SELECT CHECK_XML('value', TRUE)" + }, + { + "dialect": "snowflake", + "sql": "SELECT COMPRESS('Hello World', 'ZLIB')", + "formatted": "SELECT COMPRESS('Hello World', 'ZLIB')" + }, + { + "dialect": "snowflake", + "sql": "SELECT DECOMPRESS_BINARY('compressed_data', 'SNAPPY')", + "formatted": "SELECT DECOMPRESS_BINARY('compressed_data', 'SNAPPY')" + }, + { + "dialect": "snowflake", + "sql": "SELECT DECOMPRESS_STRING('compressed_data', 'ZSTD')", + "formatted": "SELECT DECOMPRESS_STRING('compressed_data', 'ZSTD')" + }, + { + "dialect": "snowflake", + "sql": "SELECT LPAD('Hello', 10, '*')", + "formatted": "SELECT LPAD('Hello', 10, '*')" + }, + { + "dialect": "snowflake", + "sql": "SELECT LPAD(tbl.bin_col, 10)", + "formatted": "SELECT LPAD(tbl.bin_col, 10)" + }, + { + "dialect": "snowflake", + "sql": "SELECT RPAD('Hello', 10, '*')", + "formatted": "SELECT RPAD('Hello', 10, '*')" + }, + { + "dialect": "snowflake", + "sql": "SELECT RPAD(tbl.bin_col, 10)", + "formatted": "SELECT RPAD(tbl.bin_col, 10)" + }, + { + "dialect": "snowflake", + "sql": "SELECT RPAD(TO_BINARY('Hi', 'UTF8'), 10, TO_BINARY('_', 'UTF8'))", + "formatted": "SELECT\n RPAD(\n TO_BINARY('Hi', 'UTF8'),\n 10,\n TO_BINARY('_', 'UTF8')\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT SOUNDEX(column_name)", + "formatted": "SELECT SOUNDEX(column_name)" + }, + { + "dialect": "snowflake", + "sql": "SELECT SOUNDEX_P123(column_name)", + "formatted": "SELECT SOUNDEX_P123(column_name)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ABS(x)", + "formatted": "SELECT ABS(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ASIN(0.5)", + "formatted": "SELECT ASIN(0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ASINH(0.5)", + "formatted": "SELECT ASINH(0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ATAN(0.5)", + "formatted": "SELECT ATAN(0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ATAN2(0.5, 0.3)", + "formatted": "SELECT ATAN2(0.5, 0.3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ATANH(0.5)", + "formatted": "SELECT ATANH(0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CBRT(27.0)", + "formatted": "SELECT CBRT(27.0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT POW(2, 3)", + "formatted": "SELECT POW(2, 3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT POW(2.5, 3.0)", + "formatted": "SELECT POW(2.5, 3.0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT SQUARE(2.5)", + "formatted": "SELECT SQUARE(2.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT SIGN(x)", + "formatted": "SELECT SIGN(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT COSH(1.5)", + "formatted": "SELECT COSH(1.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TANH(0.5)", + "formatted": "SELECT TANH(0.5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRANSLATE(column_name, 'abc', '123')", + "formatted": "SELECT TRANSLATE(column_name, 'abc', '123')" + }, + { + "dialect": "snowflake", + "sql": "SELECT UNICODE(column_name)", + "formatted": "SELECT UNICODE(column_name)" + }, + { + "dialect": "snowflake", + "sql": "SELECT WIDTH_BUCKET(col, 0, 100, 10)", + "formatted": "SELECT WIDTH_BUCKET(col, 0, 100, 10)" + }, + { + "dialect": "snowflake", + "sql": "SELECT PI()", + "formatted": "SELECT PI()" + }, + { + "dialect": "snowflake", + "sql": "SELECT DEGREES(PI() / 3)", + "formatted": "SELECT DEGREES(PI() / 3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT DEGREES(1)", + "formatted": "SELECT DEGREES(1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT RADIANS(180)", + "formatted": "SELECT RADIANS(180)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_AVGX(y, x)", + "formatted": "SELECT REGR_AVGX(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_AVGY(y, x)", + "formatted": "SELECT REGR_AVGY(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_COUNT(y, x)", + "formatted": "SELECT REGR_COUNT(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_INTERCEPT(y, x)", + "formatted": "SELECT REGR_INTERCEPT(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_R2(y, x)", + "formatted": "SELECT REGR_R2(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_SXX(y, x)", + "formatted": "SELECT REGR_SXX(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_SXY(y, x)", + "formatted": "SELECT REGR_SXY(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_SYY(y, x)", + "formatted": "SELECT REGR_SYY(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGR_SLOPE(y, x)", + "formatted": "SELECT REGR_SLOPE(y, x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT RANDSTR(123, 456)", + "formatted": "SELECT RANDSTR(123, 456)" + }, + { + "dialect": "snowflake", + "sql": "SELECT RANDSTR(123, RANDOM())", + "formatted": "SELECT RANDSTR(123, RANDOM())" + }, + { + "dialect": "snowflake", + "sql": "SELECT NORMAL(0, 1, RANDOM())", + "formatted": "SELECT NORMAL(0, 1, RANDOM())" + }, + { + "dialect": "snowflake", + "sql": "SELECT GROUPING_ID(a, b) AS g_id FROM x GROUP BY ROLLUP (a, b)", + "formatted": "SELECT GROUPING_ID(a, b) AS g_id\nFROM x\nGROUP BY\n ROLLUP (\n a,\n b\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT XMLGET(object_col, 'level2')", + "formatted": "SELECT XMLGET(object_col, 'level2')" + }, + { + "dialect": "snowflake", + "sql": "SELECT XMLGET(object_col, 'level3', 1)", + "formatted": "SELECT XMLGET(object_col, 'level3', 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT {*} FROM my_table", + "formatted": "SELECT {*}\nFROM my_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT {my_table.*} FROM my_table", + "formatted": "SELECT {my_table.*}\nFROM my_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT {* ILIKE 'col1%'} FROM my_table", + "formatted": "SELECT {* ILIKE 'col1%'}\nFROM my_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT {* EXCLUDE (col1)} FROM my_table", + "formatted": "SELECT {* EXCLUDE (col1)}\nFROM my_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT {* EXCLUDE (col1, col2)} FROM my_table", + "formatted": "SELECT {* EXCLUDE (col1, col2)}\nFROM my_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT a, b, COUNT(*) FROM x GROUP BY ALL LIMIT 100", + "formatted": "SELECT\n a,\n b,\n COUNT(*)\nFROM x\nGROUP BY ALL\nLIMIT 100" + }, + { + "dialect": "snowflake", + "sql": "SELECT STAR(tbl, exclude := [foo])", + "formatted": "SELECT STAR(tbl, exclude := [foo])" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST([1, 2, 3] AS VECTOR(FLOAT, 3))", + "formatted": "SELECT CAST([1, 2, 3] AS VECTOR(FLOAT, 3))" + }, + { + "dialect": "snowflake", + "sql": "SELECT VECTOR_COSINE_SIMILARITY(a, b)", + "formatted": "SELECT VECTOR_COSINE_SIMILARITY(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT VECTOR_INNER_PRODUCT(a, b)", + "formatted": "SELECT VECTOR_INNER_PRODUCT(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT VECTOR_L1_DISTANCE(a, b)", + "formatted": "SELECT VECTOR_L1_DISTANCE(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT VECTOR_L2_DISTANCE(a, b)", + "formatted": "SELECT VECTOR_L2_DISTANCE(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CONNECT_BY_ROOT test AS test_column_alias", + "formatted": "SELECT CONNECT_BY_ROOT test AS test_column_alias" + }, + { + "dialect": "snowflake", + "sql": "SELECT rename, replace", + "formatted": "SELECT\n rename,\n replace" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMEADD(HOUR, 2, CAST('09:05:03' AS TIME))", + "formatted": "SELECT TIMEADD(HOUR, 2, CAST('09:05:03' AS TIME))" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS MAP(VARCHAR, INT))", + "formatted": "SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS MAP(VARCHAR, INT))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_CAT(CAST(col AS MAP(VARCHAR, VARCHAR)), CAST(col AS MAP(VARCHAR, VARCHAR)))", + "formatted": "SELECT MAP_CAT(CAST(col AS MAP(VARCHAR, VARCHAR)), CAST(col AS MAP(VARCHAR, VARCHAR)))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_CONTAINS_KEY('k1', CAST(col AS MAP(VARCHAR, VARCHAR)))", + "formatted": "SELECT MAP_CONTAINS_KEY('k1', CAST(col AS MAP(VARCHAR, VARCHAR)))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_DELETE(CAST(col AS MAP(VARCHAR, VARCHAR)), 'k1')", + "formatted": "SELECT MAP_DELETE(CAST(col AS MAP(VARCHAR, VARCHAR)), 'k1')" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_INSERT(CAST(col AS MAP(VARCHAR, VARCHAR)), 'b', '2')", + "formatted": "SELECT MAP_INSERT(CAST(col AS MAP(VARCHAR, VARCHAR)), 'b', '2')" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_KEYS(CAST(col AS MAP(VARCHAR, VARCHAR)))", + "formatted": "SELECT MAP_KEYS(CAST(col AS MAP(VARCHAR, VARCHAR)))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_PICK(CAST(col AS MAP(VARCHAR, VARCHAR)), 'a', 'c')", + "formatted": "SELECT MAP_PICK(CAST(col AS MAP(VARCHAR, VARCHAR)), 'a', 'c')" + }, + { + "dialect": "snowflake", + "sql": "SELECT MAP_SIZE(CAST(col AS MAP(VARCHAR, VARCHAR)))", + "formatted": "SELECT MAP_SIZE(CAST(col AS MAP(VARCHAR, VARCHAR)))" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS OBJECT(a CHAR NOT NULL))", + "formatted": "SELECT CAST(OBJECT_CONSTRUCT('a', 1) AS OBJECT(a CHAR NOT NULL))" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST([1, 2, 3] AS ARRAY(INT))", + "formatted": "SELECT CAST([1, 2, 3] AS ARRAY(INT))" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(obj AS OBJECT(x CHAR) RENAME FIELDS)", + "formatted": "SELECT CAST(obj AS OBJECT(x CHAR) RENAME FIELDS)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(obj AS OBJECT(x CHAR, y VARCHAR) ADD FIELDS)", + "formatted": "SELECT CAST(obj AS OBJECT(x CHAR, y VARCHAR) ADD FIELDS)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP(x) FROM t", + "formatted": "SELECT TO_TIMESTAMP(x)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP_NTZ(x) FROM t", + "formatted": "SELECT TO_TIMESTAMP_NTZ(x)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP_LTZ(x) FROM t", + "formatted": "SELECT TO_TIMESTAMP_LTZ(x)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP_TZ(x) FROM t", + "formatted": "SELECT TO_TIMESTAMP_TZ(x)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_FROM_PARTS(2024, 5, 9, 14, 30, 45)", + "formatted": "SELECT TIMESTAMP_FROM_PARTS(2024, 5, 9, 14, 30, 45)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_FROM_PARTS(2024, 5, 9, 14, 30, 45, 123)", + "formatted": "SELECT TIMESTAMP_FROM_PARTS(2024, 5, 9, 14, 30, 45, 123)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_LTZ_FROM_PARTS(2013, 4, 5, 12, 00, 00)", + "formatted": "SELECT TIMESTAMP_LTZ_FROM_PARTS(2013, 4, 5, 12, 00, 00)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_TZ_FROM_PARTS(2013, 4, 5, 12, 00, 00)", + "formatted": "SELECT TIMESTAMP_TZ_FROM_PARTS(2013, 4, 5, 12, 00, 00)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_TZ_FROM_PARTS(2013, 4, 5, 12, 00, 00, 0, 'America/Los_Angeles')", + "formatted": "SELECT TIMESTAMP_TZ_FROM_PARTS(2013, 4, 5, 12, 00, 00, 0, 'America/Los_Angeles')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_FROM_PARTS(CAST('2024-05-09' AS DATE), CAST('14:30:45' AS TIME))", + "formatted": "SELECT\n TIMESTAMP_FROM_PARTS(\n CAST('2024-05-09' AS DATE),\n CAST('14:30:45' AS TIME)\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_NTZ_FROM_PARTS(TO_DATE('2013-04-05'), TO_TIME('12:00:00'))", + "formatted": "SELECT TIMESTAMP_NTZ_FROM_PARTS(TO_DATE('2013-04-05'), TO_TIME('12:00:00'))" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMP_NTZ_FROM_PARTS(2013, 4, 5, 12, 00, 00, 987654321)", + "formatted": "SELECT TIMESTAMP_NTZ_FROM_PARTS(2013, 4, 5, 12, 00, 00, 987654321)" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_FROM_PARTS(1977, 8, 7)", + "formatted": "SELECT DATE_FROM_PARTS(1977, 8, 7)" + }, + { + "dialect": "snowflake", + "sql": "SELECT GET_PATH(v, 'attr[0].name') FROM vartab", + "formatted": "SELECT GET_PATH(v, 'attr[0].name')\nFROM vartab" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_ARRAY(CAST(x AS ARRAY))", + "formatted": "SELECT TO_ARRAY(CAST(x AS ARRAY))" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_ARRAY(CAST(['test'] AS VARIANT))", + "formatted": "SELECT TO_ARRAY(CAST(['test'] AS VARIANT))" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_UNIQUE_AGG(x)", + "formatted": "SELECT ARRAY_UNIQUE_AGG(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_APPEND([1, 2, 3], 4)", + "formatted": "SELECT ARRAY_APPEND([1, 2, 3], 4)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_CAT([1, 2], [3, 4])", + "formatted": "SELECT ARRAY_CAT([1, 2], [3, 4])" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_PREPEND([2, 3, 4], 1)", + "formatted": "SELECT ARRAY_PREPEND([2, 3, 4], 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_REMOVE([1, 2, 3], 2)", + "formatted": "SELECT ARRAY_REMOVE([1, 2, 3], 2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAYS_ZIP([1, 2, 3])", + "formatted": "SELECT ARRAYS_ZIP([1, 2, 3])" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAYS_ZIP([1, 2, 3], ['a', 'b', 'c'], [10, 20, 30])", + "formatted": "SELECT\n ARRAYS_ZIP(\n [1, 2, 3],\n ['a', 'b', 'c'],\n [10, 20, 30]\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT AI_AGG(review, 'Summarize the reviews')", + "formatted": "SELECT AI_AGG(review, 'Summarize the reviews')" + }, + { + "dialect": "snowflake", + "sql": "SELECT AI_SUMMARIZE_AGG(review)", + "formatted": "SELECT AI_SUMMARIZE_AGG(review)" + }, + { + "dialect": "snowflake", + "sql": "SELECT AI_CLASSIFY('text', ['travel', 'cooking'])", + "formatted": "SELECT AI_CLASSIFY('text', ['travel', 'cooking'])" + }, + { + "dialect": "snowflake", + "sql": "SELECT OBJECT_CONSTRUCT()", + "formatted": "SELECT OBJECT_CONSTRUCT()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ACCOUNT()", + "formatted": "SELECT CURRENT_ACCOUNT()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ACCOUNT_NAME()", + "formatted": "SELECT CURRENT_ACCOUNT_NAME()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_AVAILABLE_ROLES()", + "formatted": "SELECT CURRENT_AVAILABLE_ROLES()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_CLIENT()", + "formatted": "SELECT CURRENT_CLIENT()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_IP_ADDRESS()", + "formatted": "SELECT CURRENT_IP_ADDRESS()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_DATABASE()", + "formatted": "SELECT CURRENT_DATABASE()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_SCHEMAS()", + "formatted": "SELECT CURRENT_SCHEMAS()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_SECONDARY_ROLES()", + "formatted": "SELECT CURRENT_SECONDARY_ROLES()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_SESSION()", + "formatted": "SELECT CURRENT_SESSION()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_STATEMENT()", + "formatted": "SELECT CURRENT_STATEMENT()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_VERSION()", + "formatted": "SELECT CURRENT_VERSION()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_TRANSACTION()", + "formatted": "SELECT CURRENT_TRANSACTION()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_WAREHOUSE()", + "formatted": "SELECT CURRENT_WAREHOUSE()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ORGANIZATION_USER()", + "formatted": "SELECT CURRENT_ORGANIZATION_USER()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_REGION()", + "formatted": "SELECT CURRENT_REGION()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ROLE()", + "formatted": "SELECT CURRENT_ROLE()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ROLE_TYPE()", + "formatted": "SELECT CURRENT_ROLE_TYPE()" + }, + { + "dialect": "snowflake", + "sql": "SELECT DAY(CURRENT_TIMESTAMP())", + "formatted": "SELECT DAY(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT DAYOFMONTH(CURRENT_TIMESTAMP())", + "formatted": "SELECT DAYOFMONTH(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT DAYOFYEAR(CURRENT_TIMESTAMP())", + "formatted": "SELECT DAYOFYEAR(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT MONTH(CURRENT_TIMESTAMP())", + "formatted": "SELECT MONTH(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT QUARTER(CURRENT_TIMESTAMP())", + "formatted": "SELECT QUARTER(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT WEEK(CURRENT_TIMESTAMP())", + "formatted": "SELECT WEEK(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT WEEKISO(CURRENT_TIMESTAMP())", + "formatted": "SELECT WEEKISO(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT YEAR(CURRENT_TIMESTAMP())", + "formatted": "SELECT YEAR(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT YEAROFWEEK(CURRENT_TIMESTAMP())", + "formatted": "SELECT YEAROFWEEK(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT YEAROFWEEKISO(CURRENT_TIMESTAMP())", + "formatted": "SELECT YEAROFWEEKISO(CURRENT_TIMESTAMP())" + }, + { + "dialect": "snowflake", + "sql": "SELECT SUM(amount) FROM mytable GROUP BY ALL", + "formatted": "SELECT SUM(amount)\nFROM mytable\nGROUP BY ALL" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV(x)", + "formatted": "SELECT STDDEV(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV(x) OVER (PARTITION BY 1)", + "formatted": "SELECT STDDEV(x) OVER (PARTITION BY 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV_POP(x)", + "formatted": "SELECT STDDEV_POP(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV_POP(x) OVER (PARTITION BY 1)", + "formatted": "SELECT STDDEV_POP(x) OVER (PARTITION BY 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV_SAMP(x)", + "formatted": "SELECT STDDEV_SAMP(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT STDDEV_SAMP(x) OVER (PARTITION BY 1)", + "formatted": "SELECT STDDEV_SAMP(x) OVER (PARTITION BY 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT KURTOSIS(x)", + "formatted": "SELECT KURTOSIS(x)" + }, + { + "dialect": "snowflake", + "sql": "SELECT KURTOSIS(x) OVER (PARTITION BY 1)", + "formatted": "SELECT KURTOSIS(x) OVER (PARTITION BY 1)" + }, + { + "dialect": "snowflake", + "sql": "WITH x AS (SELECT 1 AS foo) SELECT foo FROM IDENTIFIER('x')", + "formatted": "WITH x AS (\n SELECT 1 AS foo\n)\n\nSELECT foo\nFROM IDENTIFIER('x')" + }, + { + "dialect": "snowflake", + "sql": "WITH x AS (SELECT 1 AS foo) SELECT IDENTIFIER('foo') FROM x", + "formatted": "WITH x AS (\n SELECT 1 AS foo\n)\n\nSELECT IDENTIFIER('foo')\nFROM x" + }, + { + "dialect": "snowflake", + "sql": "SELECT IDENTIFIER($my_function_name)()", + "formatted": "SELECT IDENTIFIER($my_function_name)()" + }, + { + "dialect": "snowflake", + "sql": "SELECT IDENTIFIER('speed_of_light')()", + "formatted": "SELECT IDENTIFIER('speed_of_light')()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST('2021-01-01' AS DATE) + INTERVAL '1 DAY'", + "formatted": "SELECT CAST('2021-01-01' AS DATE) + INTERVAL '1 DAY'" + }, + { + "dialect": "snowflake", + "sql": "SELECT HLL(*)", + "formatted": "SELECT HLL(*)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HLL(a)", + "formatted": "SELECT HLL(a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HLL(DISTINCT t.a)", + "formatted": "SELECT HLL(DISTINCT t.a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HLL(a, b, c)", + "formatted": "SELECT HLL(a, b, c)" + }, + { + "dialect": "snowflake", + "sql": "SELECT HLL(DISTINCT a, b, c)", + "formatted": "SELECT HLL(DISTINCT a, b, c)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_LIKE(a, b, c)", + "formatted": "SELECT REGEXP_LIKE(a, b, c)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CONVERT_TIMEZONE('UTC', 'America/Los_Angeles', col)", + "formatted": "SELECT CONVERT_TIMEZONE('UTC', 'America/Los_Angeles', col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_ORGANIZATION_NAME()", + "formatted": "SELECT CURRENT_ORGANIZATION_NAME()" + }, + { + "dialect": "snowflake", + "sql": "SELECT MATCH_CONDITION", + "formatted": "SELECT MATCH_CONDITION" + }, + { + "dialect": "snowflake", + "sql": "SELECT OBJECT_AGG(key, value) FROM tbl", + "formatted": "SELECT OBJECT_AGG(key, value)\nFROM tbl" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_CHAR(CAST('12:05:05' AS TIME))", + "formatted": "SELECT TO_CHAR(CAST('12:05:05' AS TIME))" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRIM(COALESCE(TO_CHAR(CAST(c AS TIME)), '')) FROM t", + "formatted": "SELECT TRIM(COALESCE(TO_CHAR(CAST(c AS TIME)), ''))\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT GET_PATH(PARSE_JSON(foo), 'bar')", + "formatted": "SELECT GET_PATH(PARSE_JSON(foo), 'bar')" + }, + { + "dialect": "snowflake", + "sql": "SELECT PARSE_IP('192.168.1.1', 'INET')", + "formatted": "SELECT PARSE_IP('192.168.1.1', 'INET')" + }, + { + "dialect": "snowflake", + "sql": "SELECT PARSE_IP('192.168.1.1', 'INET', 0)", + "formatted": "SELECT PARSE_IP('192.168.1.1', 'INET', 0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT GET_PATH(foo, 'bar')", + "formatted": "SELECT GET_PATH(foo, 'bar')" + }, + { + "dialect": "snowflake", + "sql": "SELECT a, exclude, b FROM xxx", + "formatted": "SELECT\n a,\n exclude,\n b\nFROM xxx" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_SORT(x, TRUE, FALSE)", + "formatted": "SELECT ARRAY_SORT(x, TRUE, FALSE)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BOOLXOR_AGG(col) FROM tbl", + "formatted": "SELECT BOOLXOR_AGG(col)\nFROM tbl" + }, + { + "dialect": "snowflake", + "sql": "SELECT PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY col) OVER (PARTITION BY category)", + "formatted": "SELECT PERCENTILE_DISC(0.9) WITHIN GROUP (ORDER BY col) OVER (PARTITION BY category)" + }, + { + "dialect": "snowflake", + "sql": "SELECT AI_CLASSIFY('text', ['travel', 'cooking'], OBJECT_CONSTRUCT('output_mode', 'multi'))", + "formatted": "SELECT\n AI_CLASSIFY(\n 'text',\n ['travel', 'cooking'],\n OBJECT_CONSTRUCT('output_mode', 'multi')\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM table AT (TIMESTAMP => '2024-07-24') UNPIVOT(a FOR b IN (c)) AS pivot_table", + "formatted": "SELECT *\nFROM table AT (TIMESTAMP => '2024-07-24') UNPIVOT(a FOR b IN (c)) AS pivot_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN ('2023_Q1', '2023_Q2', '2023_Q3', '2023_Q4', '2024_Q1') DEFAULT ON NULL (0)) ORDER BY empid", + "formatted": "SELECT *\nFROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN ('2023_Q1', '2023_Q2', '2023_Q3', '2023_Q4', '2024_Q1') DEFAULT ON NULL (0))\nORDER BY\n empid" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (SELECT DISTINCT quarter FROM ad_campaign_types_by_quarter WHERE television = TRUE ORDER BY quarter)) ORDER BY empid", + "formatted": "SELECT *\nFROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (SELECT DISTINCT\n quarter\nFROM ad_campaign_types_by_quarter\nWHERE television = TRUE\nORDER BY\n quarter))\nORDER BY\n empid" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (ANY ORDER BY quarter)) ORDER BY empid", + "formatted": "SELECT *\nFROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (ANY ORDER BY quarter))\nORDER BY\n empid" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (ANY)) ORDER BY empid", + "formatted": "SELECT *\nFROM quarterly_sales PIVOT(SUM(amount) FOR quarter IN (ANY))\nORDER BY\n empid" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM DATA AS DATA_L ASOF JOIN DATA AS DATA_R MATCH_CONDITION (DATA_L.VAL > DATA_R.VAL) ON DATA_L.ID = DATA_R.ID", + "formatted": "SELECT *\nFROM DATA AS DATA_L\nASOF JOIN DATA AS DATA_R MATCH_CONDITION (DATA_L.VAL > DATA_R.VAL)\n ON DATA_L.ID = DATA_R.ID" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP('2025-01-16T14:45:30.123+0500', 'yyyy-mm-DDThh24:mi:ss.ff9tzhtzm')", + "formatted": "SELECT TO_TIMESTAMP('2025-01-16T14:45:30.123+0500', 'yyyy-mm-DDThh24:mi:ss.ff9tzhtzm')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * REPLACE (CAST(col AS TEXT) AS scol) FROM t", + "formatted": "SELECT * REPLACE (CAST(col AS TEXT) AS scol)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 put", + "formatted": "SELECT 1 put", + "previously_formatted": "SELECT\n 1 AS put" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 get", + "formatted": "SELECT 1 get", + "previously_formatted": "SELECT\n 1 AS get" + }, + { + "dialect": "snowflake", + "sql": "WITH t (SELECT 1 AS c) SELECT c FROM t", + "formatted": "WITH t (\n SELECT 1 AS c\n)\nSELECT c\nFROM t", + "previously_formatted": "WITH t AS (\n SELECT\n 1 AS c\n)\nSELECT\n c\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM s WHERE c NOT IN (1, 2, 3)", + "formatted": "SELECT *\nFROM s\nWHERE c NOT IN (1, 2, 3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM s WHERE c NOT IN (SELECT * FROM t)", + "formatted": "SELECT *\nFROM s\nWHERE\n c NOT IN (\n SELECT *\n FROM t\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM t1 INNER JOIN t2 USING (t1.col)", + "formatted": "SELECT *\nFROM t1\nINNER JOIN t2\n USING (t1.col)" + }, + { + "dialect": "snowflake", + "sql": "SELECT a:from::STRING, a:from || ' test' ", + "formatted": "SELECT\n a:from::STRING,\n a:from || ' test'" + }, + { + "dialect": "snowflake", + "sql": "SELECT a:select", + "formatted": "SELECT a:select" + }, + { + "dialect": "snowflake", + "sql": "SELECT GET_PATH(PARSE_JSON('{\"y\": [{\"z\": 1}]}'), 'y[0]:z')", + "formatted": "SELECT GET_PATH(PARSE_JSON('{\"y\": [{\"z\": 1}]}'), 'y[0]:z')" + }, + { + "dialect": "snowflake", + "sql": "SELECT p FROM t WHERE p:val NOT IN ('2')", + "formatted": "SELECT p\nFROM t\nWHERE p:val NOT IN ('2')" + }, + { + "dialect": "snowflake", + "sql": "SELECT PARSE_JSON('{\"x\": \"hello\"}'):x LIKE 'hello'", + "formatted": "SELECT PARSE_JSON('{\"x\": \"hello\"}'):x LIKE 'hello'" + }, + { + "dialect": "snowflake", + "sql": "SELECT data:x LIKE 'hello' FROM some_table", + "formatted": "SELECT data:x LIKE 'hello'\nFROM some_table" + }, + { + "dialect": "snowflake", + "sql": "SELECT SUM({ fn CONVERT(123, SQL_DOUBLE) })", + "formatted": "SELECT SUM({ fn CONVERT(123, SQL_DOUBLE) })" + }, + { + "dialect": "snowflake", + "sql": "SELECT SUM({ fn CONVERT(123, SQL_VARCHAR) })", + "formatted": "SELECT SUM({ fn CONVERT(123, SQL_VARCHAR) })" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMPFROMPARTS(d, t)", + "formatted": "SELECT TIMESTAMPFROMPARTS(d, t)" + }, + { + "dialect": "snowflake", + "sql": "SELECT v:attr[0].name FROM vartab", + "formatted": "SELECT v:attr[0].name\nFROM vartab" + }, + { + "dialect": "snowflake", + "sql": "SELECT v:\"fruit\" FROM vartab", + "formatted": "SELECT v:\"fruit\"\nFROM vartab" + }, + { + "dialect": "snowflake", + "sql": "SELECT PARSE_JSON('{\"food\":{\"fruit\":\"banana\"}}'):food.fruit::VARCHAR", + "formatted": "SELECT PARSE_JSON('{\"food\":{\"fruit\":\"banana\"}}'):food.fruit::VARCHAR" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM t, UNNEST(x) WITH ORDINALITY", + "formatted": "SELECT *\nFROM t, UNNEST(x) WITH ORDINALITY" + }, + { + "dialect": "snowflake", + "sql": "SELECT state, city, SUM(retail_price * quantity) AS gross_revenue FROM sales GROUP BY ALL", + "formatted": "SELECT\n state,\n city,\n SUM(retail_price * quantity) AS gross_revenue\nFROM sales\nGROUP BY ALL" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM foo window", + "formatted": "SELECT *\nFROM foo window", + "previously_formatted": "SELECT\n *\nFROM foo AS window" + }, + { + "dialect": "snowflake", + "sql": "SELECT RLIKE(a, $$regular expression with \\ characters: \\d{2}-\\d{3}-\\d{4}$$, 'i') FROM log_source", + "formatted": "SELECT RLIKE(a, $$regular expression with \\ characters: \\d{2}-\\d{3}-\\d{4}$$, 'i')\nFROM log_source" + }, + { + "dialect": "snowflake", + "sql": "SELECT $$a ' \\ \\t \\x21 z $ $$", + "formatted": "SELECT $$a ' \\ \\t \\x21 z $ $$" + }, + { + "dialect": "snowflake", + "sql": "SELECT $$a\\$b$$", + "formatted": "SELECT $$a\\$b$$" + }, + { + "dialect": "snowflake", + "sql": "SELECT $$a\\$$", + "formatted": "SELECT $$a\\$$" + }, + { + "dialect": "snowflake", + "sql": "SELECT {'test': 'best'}::VARIANT", + "formatted": "SELECT {'test': 'best'}::VARIANT" + }, + { + "dialect": "snowflake", + "sql": "SELECT {fn DAYNAME('2022-5-13')}", + "formatted": "SELECT {fn\n DAYNAME('2022-5-13') }" + }, + { + "dialect": "snowflake", + "sql": "SELECT {fn LOG(5)}", + "formatted": "SELECT {fn\n LOG(5) }" + }, + { + "dialect": "snowflake", + "sql": "SELECT {fn CEILING(5.3)}", + "formatted": "SELECT {fn\n CEILING(5.3) }" + }, + { + "dialect": "snowflake", + "sql": "SELECT CEIL(3.14)", + "formatted": "SELECT CEIL(3.14)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CEIL(3.14, 1)", + "formatted": "SELECT CEIL(3.14, 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT DAYOFWEEK('2016-01-02T23:39:20.123-07:00'::TIMESTAMP)", + "formatted": "SELECT DAYOFWEEK('2016-01-02T23:39:20.123-07:00'::TIMESTAMP)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM xxx WHERE col ilike '%Don''t%'", + "formatted": "SELECT *\nFROM xxx\nWHERE col ILIKE '%Don''t%'" + }, + { + "dialect": "snowflake", + "sql": "SELECT * EXCLUDE a, b FROM xxx", + "formatted": "SELECT\n * EXCLUDE a,\n b\nFROM xxx" + }, + { + "dialect": "snowflake", + "sql": "SELECT * RENAME a AS b, c AS d FROM xxx", + "formatted": "SELECT\n * RENAME a AS b,\n c AS d\nFROM xxx" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM xxx, yyy, zzz,", + "formatted": "SELECT *\nFROM xxx, yyy, zzz," + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM xxx, yyy, zzz, WHERE foo = bar", + "formatted": "SELECT *\nFROM xxx, yyy, zzz,\nWHERE foo = bar" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM xxx, yyy, zzz", + "formatted": "SELECT *\nFROM xxx, yyy, zzz" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIMESTAMPNTZFROMPARTS(2013, 4, 5, 12, 00, 00)", + "formatted": "SELECT TIMESTAMPNTZFROMPARTS(2013, 4, 5, 12, 00, 00)" + }, + { + "dialect": "snowflake", + "sql": "SELECT NTH_VALUE(a, 2) FROM t", + "formatted": "SELECT NTH_VALUE(a, 2)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_CONSTRUCT('foo')::VARIANT[0]", + "formatted": "SELECT ARRAY_CONSTRUCT('foo')::VARIANT[0]" + }, + { + "dialect": "snowflake", + "sql": "SELECT UUID_STRING(), UUID_STRING('fe971b24-9572-4005-b22f-351e9c09274d', 'foo')", + "formatted": "SELECT\n UUID_STRING(),\n UUID_STRING('fe971b24-9572-4005-b22f-351e9c09274d', 'foo')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITNOT(a)", + "formatted": "SELECT BITNOT(a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_NOT(a)", + "formatted": "SELECT BIT_NOT(a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITAND(a, b)", + "formatted": "SELECT BITAND(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITAND(a, b, 'LEFT')", + "formatted": "SELECT BITAND(a, b, 'LEFT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_AND(a, b)", + "formatted": "SELECT BIT_AND(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_AND(a, b, 'LEFT')", + "formatted": "SELECT BIT_AND(a, b, 'LEFT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITOR(a, b)", + "formatted": "SELECT BITOR(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITOR(a, b, 'LEFT')", + "formatted": "SELECT BITOR(a, b, 'LEFT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_OR(a, b)", + "formatted": "SELECT BIT_OR(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_OR(a, b, 'RIGHT')", + "formatted": "SELECT BIT_OR(a, b, 'RIGHT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITXOR(a, b)", + "formatted": "SELECT BITXOR(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BITXOR(a, b, 'LEFT')", + "formatted": "SELECT BITXOR(a, b, 'LEFT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_XOR(a, b)", + "formatted": "SELECT BIT_XOR(a, b)" + }, + { + "dialect": "snowflake", + "sql": "SELECT BIT_XOR(a, b, 'LEFT')", + "formatted": "SELECT BIT_XOR(a, b, 'LEFT')" + }, + { + "dialect": "snowflake", + "sql": "SELECT HOUR(CAST('08:50:57' AS TIME))", + "formatted": "SELECT HOUR(CAST('08:50:57' AS TIME))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MINUTE(CAST('08:50:57' AS TIME))", + "formatted": "SELECT MINUTE(CAST('08:50:57' AS TIME))" + }, + { + "dialect": "snowflake", + "sql": "SELECT SECOND(CAST('08:50:57' AS TIME))", + "formatted": "SELECT SECOND(CAST('08:50:57' AS TIME))" + }, + { + "dialect": "snowflake", + "sql": "SELECT HOUR(CAST('2024-05-09 08:50:57' AS TIMESTAMP))", + "formatted": "SELECT HOUR(CAST('2024-05-09 08:50:57' AS TIMESTAMP))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MONTHNAME(CAST('2024-05-09' AS DATE))", + "formatted": "SELECT MONTHNAME(CAST('2024-05-09' AS DATE))" + }, + { + "dialect": "snowflake", + "sql": "SELECT PREVIOUS_DAY(CAST('2024-05-09' AS DATE), 'MONDAY')", + "formatted": "SELECT PREVIOUS_DAY(CAST('2024-05-09' AS DATE), 'MONDAY')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIME_FROM_PARTS(14, 30, 45)", + "formatted": "SELECT TIME_FROM_PARTS(14, 30, 45)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIME_FROM_PARTS(14, 30, 45, 123)", + "formatted": "SELECT TIME_FROM_PARTS(14, 30, 45, 123)" + }, + { + "dialect": "snowflake", + "sql": "SELECT MONTHS_BETWEEN(CAST('2019-03-15' AS DATE), CAST('2019-02-15' AS DATE))", + "formatted": "SELECT MONTHS_BETWEEN(CAST('2019-03-15' AS DATE), CAST('2019-02-15' AS DATE))" + }, + { + "dialect": "snowflake", + "sql": "SELECT MONTHS_BETWEEN(CAST('2019-03-01 02:00:00' AS TIMESTAMP), CAST('2019-02-15 01:00:00' AS TIMESTAMP))", + "formatted": "SELECT MONTHS_BETWEEN(CAST('2019-03-01 02:00:00' AS TIMESTAMP), CAST('2019-02-15 01:00:00' AS TIMESTAMP))" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIME_SLICE(CAST('2024-05-09 08:50:57.891' AS TIMESTAMP), 15, 'MINUTE')", + "formatted": "SELECT TIME_SLICE(CAST('2024-05-09 08:50:57.891' AS TIMESTAMP), 15, 'MINUTE')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIME_SLICE(CAST('2024-05-09' AS DATE), 1, 'DAY')", + "formatted": "SELECT TIME_SLICE(CAST('2024-05-09' AS DATE), 1, 'DAY')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TIME_SLICE(CAST('2024-05-09 08:50:57.891' AS TIMESTAMP), 1, 'HOUR', 'start')", + "formatted": "SELECT\n TIME_SLICE(\n CAST('2024-05-09 08:50:57.891' AS TIMESTAMP),\n 1,\n 'HOUR',\n 'start'\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT * EXCLUDE foo RENAME bar AS baz FROM tbl", + "formatted": "SELECT * EXCLUDE foo RENAME bar AS baz\nFROM tbl" + }, + { + "dialect": "snowflake", + "sql": "SELECT LIKE(col, 'pattern')", + "formatted": "SELECT LIKE(col, 'pattern')" + }, + { + "dialect": "snowflake", + "sql": "SELECT ILIKE(col, 'pattern')", + "formatted": "SELECT ILIKE(col, 'pattern')" + }, + { + "dialect": "snowflake", + "sql": "SELECT LIKE(col, 'pattern', '\\\\')", + "formatted": "SELECT LIKE(col, 'pattern', '\\\\')" + }, + { + "dialect": "snowflake", + "sql": "SELECT ILIKE(col, 'pattern', '\\\\')", + "formatted": "SELECT ILIKE(col, 'pattern', '\\\\')" + }, + { + "dialect": "snowflake", + "sql": "SELECT LIKE(col, 'pattern', '!')", + "formatted": "SELECT LIKE(col, 'pattern', '!')" + }, + { + "dialect": "snowflake", + "sql": "SELECT ILIKE(col, 'pattern', '!')", + "formatted": "SELECT ILIKE(col, 'pattern', '!')" + }, + { + "dialect": "snowflake", + "sql": "SELECT BASE64_ENCODE('Hello World')", + "formatted": "SELECT BASE64_ENCODE('Hello World')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRY_HEX_DECODE_BINARY('48656C6C6F')", + "formatted": "SELECT TRY_HEX_DECODE_BINARY('48656C6C6F')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRY_HEX_DECODE_STRING('48656C6C6F')", + "formatted": "SELECT TRY_HEX_DECODE_STRING('48656C6C6F')" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_CONTAINS(1, [1])", + "formatted": "SELECT ARRAY_CONTAINS(1, [1])" + }, + { + "dialect": "snowflake", + "sql": "SELECT ARRAY_DISTINCT(['A', 'B', 'A'])", + "formatted": "SELECT ARRAY_DISTINCT(['A', 'B', 'A'])" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_DATABASE()", + "formatted": "SELECT CURRENT_DATABASE()" + }, + { + "dialect": "snowflake", + "sql": "SELECT CURRENT_SCHEMA()", + "formatted": "SELECT CURRENT_SCHEMA()" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @\"mystage\"", + "formatted": "SELECT *\nFROM @\"mystage\"" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @\"myschema\".\"mystage\"/file.gz", + "formatted": "SELECT *\nFROM @\"myschema\".\"mystage\"/file.gz" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @\"my_DB\".\"schEMA1\".mystage/file.gz", + "formatted": "SELECT *\nFROM @\"my_DB\".\"schEMA1\".mystage/file.gz" + }, + { + "dialect": "snowflake", + "sql": "SELECT metadata$filename FROM @s1/", + "formatted": "SELECT metadata$filename\nFROM @s1/" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @~", + "formatted": "SELECT *\nFROM @~" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @~/some/path/to/file.csv", + "formatted": "SELECT *\nFROM @~/some/path/to/file.csv" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @mystage", + "formatted": "SELECT *\nFROM @mystage" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM '@mystage'", + "formatted": "SELECT *\nFROM '@mystage'" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @namespace.mystage/path/to/file.json.gz", + "formatted": "SELECT *\nFROM @namespace.mystage/path/to/file.json.gz" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @namespace.%table_name/path/to/file.json.gz", + "formatted": "SELECT *\nFROM @namespace.%table_name/path/to/file.json.gz" + }, + { + "dialect": "snowflake", + "sql": "SELECT $1, $2, metadata$filename FROM @mystage (PATTERN => '.*data-100.*')", + "formatted": "SELECT\n $1,\n $2,\n metadata$filename\nFROM @mystage (PATTERN => '.*data-100.*')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM '@external/location' (FILE_FORMAT => 'path.to.csv')", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM (SELECT a FROM @foo)", + "formatted": "SELECT *\nFROM (\n SELECT a\n FROM @foo\n)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM (SELECT * FROM '@external/location' (FILE_FORMAT => 'path.to.csv'))", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @foo/bar (FILE_FORMAT => ds_sandbox.test.my_csv_format, PATTERN => 'test') AS bla", + "formatted": "SELECT *\nFROM @foo/bar (FILE_FORMAT => ds_sandbox.test.my_csv_format, PATTERN => 'test') AS bla" + }, + { + "dialect": "snowflake", + "sql": "SELECT t.$1, t.$2 FROM @mystage1 (FILE_FORMAT => 'myformat', PATTERN => '.*data.*[.]csv.gz') AS t", + "formatted": "SELECT\n t.$1,\n t.$2\nFROM @mystage1 (FILE_FORMAT => 'myformat', PATTERN => '.*data.*[.]csv.gz') AS t" + }, + { + "dialect": "snowflake", + "sql": "SELECT parse_json($1):a.b FROM @mystage2/data1.json.gz", + "formatted": "SELECT parse_json($1):a.b\nFROM @mystage2/data1.json.gz" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @mystage t (c1)", + "formatted": "SELECT *\nFROM @mystage t(c1)", + "previously_formatted": "SELECT\n *\nFROM @mystage AS t(c1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @foo/bar (PATTERN => 'test', FILE_FORMAT => ds_sandbox.test.my_csv_format) AS bla", + "formatted": "SELECT *\nFROM @foo/bar (PATTERN => 'test', FILE_FORMAT => ds_sandbox.test.my_csv_format) AS bla" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM @test.public.thing/location/somefile.csv( FILE_FORMAT => 'fmt' )", + "formatted": "SELECT *\nFROM @test.public.thing/location/somefile.csv (FILE_FORMAT => 'fmt')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable TABLESAMPLE BERNOULLI (20.3)", + "formatted": "SELECT *\nFROM testtable TABLESAMPLE BERNOULLI (20.3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable TABLESAMPLE SYSTEM (3) SEED (82)", + "formatted": "SELECT *\nFROM testtable TABLESAMPLE SYSTEM (3) SEED (82)" + }, + { + "dialect": "snowflake", + "sql": "SELECT a FROM test PIVOT(SUM(x) FOR y IN ('z', 'q')) AS x TABLESAMPLE BERNOULLI (0.1)", + "formatted": "SELECT a\nFROM test PIVOT(SUM(x) FOR y IN ('z', 'q')) AS x TABLESAMPLE BERNOULLI (0.1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT i, j FROM table1 AS t1 INNER JOIN table2 AS t2 TABLESAMPLE BERNOULLI (50) WHERE t2.j = t1.i", + "formatted": "SELECT\n i,\n j\nFROM table1 AS t1\nINNER JOIN table2 AS t2 TABLESAMPLE BERNOULLI (50)\nWHERE t2.j = t1.i" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM (SELECT * FROM t1 JOIN t2 ON t1.a = t2.c) TABLESAMPLE BERNOULLI (1)", + "formatted": "SELECT *\nFROM (\n SELECT *\n FROM t1\n JOIN t2\n ON t1.a = t2.c\n) TABLESAMPLE BERNOULLI (1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable TABLESAMPLE (10 ROWS)", + "formatted": "SELECT *\nFROM testtable TABLESAMPLE (10 ROWS)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable TABLESAMPLE (100)", + "formatted": "SELECT *\nFROM testtable TABLESAMPLE (100)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable SAMPLE (10)", + "formatted": "SELECT *\nFROM testtable SAMPLE (10)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM testtable SAMPLE ROW (0)", + "formatted": "SELECT *\nFROM testtable SAMPLE ROW (0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT a FROM test SAMPLE BLOCK (0.5) SEED (42)", + "formatted": "SELECT a\nFROM test SAMPLE BLOCK (0.5) SEED (42)" + }, + { + "dialect": "snowflake", + "sql": "SELECT user_id, value FROM table_name SAMPLE BERNOULLI ($s) SEED (0)", + "formatted": "SELECT\n user_id,\n value\nFROM table_name SAMPLE BERNOULLI ($s) SEED (0)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST('12:00:00' AS TIME)", + "formatted": "SELECT CAST('12:00:00' AS TIME)" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_PART(month, a)", + "formatted": "SELECT DATE_PART(month, a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIME(x) FROM t", + "formatted": "SELECT TO_TIME(x)\nFROM t" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_DATE('2019-02-28') + INTERVAL '1 day, 1 year'", + "formatted": "SELECT TO_DATE('2019-02-28') + INTERVAL '1 day, 1 year'" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(a AS VARIANT)", + "formatted": "SELECT CAST(a AS VARIANT)" + }, + { + "dialect": "snowflake", + "sql": "SELECT CAST(a AS ARRAY)", + "formatted": "SELECT CAST(a AS ARRAY)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (STATEMENT => $query_id_var)", + "formatted": "SELECT *\nFROM my_table AT (STATEMENT => $query_id_var)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (OFFSET => -60 * 5)", + "formatted": "SELECT *\nFROM my_table AT (OFFSET => -60 * 5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table BEFORE (STATEMENT => $query_id_var)", + "formatted": "SELECT *\nFROM my_table BEFORE (STATEMENT => $query_id_var)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table BEFORE (OFFSET => -60 * 5)", + "formatted": "SELECT *\nFROM my_table BEFORE (OFFSET => -60 * 5)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (TIMESTAMP => TO_TIMESTAMP(1432669154242, 3))", + "formatted": "SELECT *\nFROM my_table AT (TIMESTAMP => TO_TIMESTAMP(1432669154242, 3))" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (OFFSET => -60 * 5) AS T WHERE T.flag = 'valid'", + "formatted": "SELECT *\nFROM my_table AT (OFFSET => -60 * 5) AS T\nWHERE T.flag = 'valid'" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726')", + "formatted": "SELECT *\nFROM my_table AT (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table BEFORE (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726')", + "formatted": "SELECT *\nFROM my_table BEFORE (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp)", + "formatted": "SELECT *\nFROM my_table AT (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table AT(TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp_tz)", + "formatted": "SELECT *\nFROM my_table AT (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp_tz)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM my_table BEFORE (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp_tz);", + "formatted": "SELECT *\nFROM my_table BEFORE (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp_tz);", + "previously_formatted": "SELECT\n *\nFROM my_table BEFORE (TIMESTAMP => 'Fri, 01 May 2015 16:20:00 -0700'::timestamp_tz)" + }, + { + "dialect": "snowflake", + "sql": "\n SELECT oldt.* , newt.*\n FROM my_table BEFORE(STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS oldt\n FULL OUTER JOIN my_table AT(STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS newt\n ON oldt.id = newt.id\n WHERE oldt.id IS NULL OR newt.id IS NULL;\n ", + "formatted": "SELECT\n oldt.*,\n newt.*\nFROM my_table BEFORE (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS oldt\nFULL OUTER JOIN my_table AT (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS newt\n ON oldt.id = newt.id\nWHERE\n oldt.id IS NULL\n OR newt.id IS NULL;", + "previously_formatted": "SELECT\n oldt.*,\n newt.*\nFROM my_table BEFORE (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS oldt\nFULL OUTER JOIN my_table AT (STATEMENT => '8e5d0ca9-005e-44e6-b858-a8f5b37c5726') AS newt\n ON oldt.id = newt.id\nWHERE\n oldt.id IS NULL\n OR newt.id IS NULL" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('MYTABLE')", + "formatted": "SELECT *\nFROM TABLE('MYTABLE')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE($MYVAR)", + "formatted": "SELECT *\nFROM TABLE($MYVAR)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE(?)", + "formatted": "SELECT *\nFROM TABLE(?)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE(:BINDING)", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE($MYVAR) WHERE COL1 = 10", + "formatted": "SELECT *\nFROM TABLE($MYVAR)\nWHERE COL1 = 10" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('t1') AS f", + "formatted": "SELECT *\nFROM TABLE('t1') AS f" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM (TABLE('t1') CROSS JOIN TABLE('t2'))", + "formatted": "SELECT *\nFROM (TABLE('t1')\nCROSS JOIN TABLE('t2'))" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('t1'), LATERAL (SELECT * FROM t2)", + "formatted": "SELECT *\nFROM TABLE('t1'), LATERAL (\n SELECT *\n FROM t2\n)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('t1') UNION ALL SELECT * FROM TABLE('t2')", + "formatted": "SELECT *\nFROM TABLE('t1')\nUNION ALL\nSELECT *\nFROM TABLE('t2')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('t1') TABLESAMPLE BERNOULLI (20.3)", + "formatted": "SELECT *\nFROM TABLE('t1') TABLESAMPLE BERNOULLI (20.3)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE('MYDB.\"MYSCHEMA\".\"MYTABLE\"')", + "formatted": "SELECT *\nFROM TABLE('MYDB.\"MYSCHEMA\".\"MYTABLE\"')" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE($$MYDB. \"MYSCHEMA\".\"MYTABLE\"$$)", + "formatted": "SELECT *\nFROM TABLE($$MYDB. \"MYSCHEMA\".\"MYTABLE\"$$)" + }, + { + "dialect": "snowflake", + "sql": "SELECT value FROM TABLE(FLATTEN(input => SELECT PARSE_JSON('[1, 2]')))", + "formatted": "SELECT value\nFROM TABLE(FLATTEN(input => SELECT\n PARSE_JSON('[1, 2]')))" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH((play, line), 'dream')", + "formatted": "SELECT SEARCH((play, line), 'dream')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(line, 'king', ANALYZER => 'UNICODE_ANALYZER')", + "formatted": "SELECT SEARCH(line, 'king', ANALYZER => 'UNICODE_ANALYZER')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(character, 'king queen', SEARCH_MODE => 'AND')", + "formatted": "SELECT SEARCH(character, 'king queen', SEARCH_MODE => 'AND')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(line, 'king', ANALYZER => 'UNICODE_ANALYZER', SEARCH_MODE => 'OR')", + "formatted": "SELECT\n SEARCH(\n line,\n 'king',\n ANALYZER => 'UNICODE_ANALYZER',\n SEARCH_MODE => 'OR'\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(line, 'king')", + "formatted": "SELECT SEARCH(line, 'king')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(line, 'king', ANALYZER => 'UNICODE_ANALYZER')", + "formatted": "SELECT SEARCH(line, 'king', ANALYZER => 'UNICODE_ANALYZER')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(character, 'king queen', SEARCH_MODE => 'AND')", + "formatted": "SELECT SEARCH(character, 'king queen', SEARCH_MODE => 'AND')" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH(line, 'king', SEARCH_MODE => 'AND', ANALYZER => 'PATTERN_ANALYZER')", + "formatted": "SELECT\n SEARCH(\n line,\n 'king',\n SEARCH_MODE => 'AND',\n ANALYZER => 'PATTERN_ANALYZER'\n )" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_COUNT('hello world', 'l ')", + "formatted": "SELECT REGEXP_COUNT('hello world', 'l ')" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_COUNT('hello world', 'l', 1)", + "formatted": "SELECT REGEXP_COUNT('hello world', 'l', 1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_COUNT('hello world', 'l', 1, 'i')", + "formatted": "SELECT REGEXP_COUNT('hello world', 'l', 1, 'i')" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_INSTR('abc', 'a')", + "formatted": "SELECT REGEXP_INSTR('abc', 'a')" + }, + { + "dialect": "snowflake", + "sql": "SELECT REGEXP_INSTR('abc', 'a', 1, 1, 0, 'i')", + "formatted": "SELECT REGEXP_INSTR('abc', 'a', 1, 1, 0, 'i')" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRY_CAST(x AS DOUBLE)", + "formatted": "SELECT TRY_CAST(x AS DOUBLE)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRY_CAST(FOO() AS TEXT)", + "formatted": "SELECT TRY_CAST(FOO() AS TEXT)" + }, + { + "dialect": "snowflake", + "sql": "SELECT $1", + "formatted": "SELECT $1" + }, + { + "dialect": "snowflake", + "sql": "SELECT $1.elem", + "formatted": "SELECT $1.elem" + }, + { + "dialect": "snowflake", + "sql": "SELECT $1:a.b", + "formatted": "SELECT $1:a.b" + }, + { + "dialect": "snowflake", + "sql": "SELECT t.$23:a.b", + "formatted": "SELECT t.$23:a.b" + }, + { + "dialect": "snowflake", + "sql": "SELECT t.$17:a[0].b[0].c", + "formatted": "SELECT t.$17:a[0].b[0].c" + }, + { + "dialect": "snowflake", + "sql": "SELECT C1 FROM t1 CHANGES (INFORMATION => APPEND_ONLY) AT (STREAM => 's1') END (TIMESTAMP => $ts2)", + "formatted": "SELECT C1\nFROM t1 CHANGES (INFORMATION => APPEND_ONLY) AT (STREAM => 's1') END (TIMESTAMP => $ts2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT C1 FROM t1 CHANGES (INFORMATION => APPEND_ONLY) BEFORE (STATEMENT => 'STMT_ID') END (TIMESTAMP => $ts2)", + "formatted": "SELECT C1\nFROM t1 CHANGES (INFORMATION => APPEND_ONLY) BEFORE (STATEMENT => 'STMT_ID') END (TIMESTAMP => $ts2)" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM some_table CHANGES (INFORMATION => APPEND_ONLY) AT (TIMESTAMP => TO_TIMESTAMP_TZ('2024-07-01 00:00:00+00:00')) END (TIMESTAMP => TO_TIMESTAMP_TZ('2024-07-01 14:28:59.999999+00:00'))", + "formatted": "SELECT 1\nFROM some_table CHANGES (INFORMATION => APPEND_ONLY) AT (TIMESTAMP => TO_TIMESTAMP_TZ('2024-07-01 00:00:00+00:00')) END (TIMESTAMP => TO_TIMESTAMP_TZ('2024-07-01 14:28:59.999999+00:00'))" + }, + { + "dialect": "snowflake", + "sql": "SELECT :1", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT :1, :2", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT :1 + :2", + "formatted": null + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM SEMANTIC_VIEW(foo METRICS a.b, a.c DIMENSIONS a.b, a.c WHERE a.b > '1995-01-01')", + "formatted": "SELECT *\nFROM SEMANTIC_VIEW(\n foo\n METRICS a.b, a.c\n DIMENSIONS a.b, a.c\n WHERE a.b > '1995-01-01'\n)" + }, + { + "dialect": "snowflake", + "sql": "SELECT col1, col2, metric1 FROM SEMANTIC_VIEW(mydb.myschema.my_semantic_view METRICS metric1 DIMENSIONS col1, DATE_TRUNC('MONTH', timestamp_col) AS col2) ORDER BY col1, col2 DESC", + "formatted": "SELECT\n col1,\n col2,\n metric1\nFROM SEMANTIC_VIEW(\n mydb.myschema.my_semantic_view\n METRICS metric1\n DIMENSIONS col1, DATE_TRUNC('MONTH', timestamp_col) AS col2\n)\nORDER BY\n col1,\n col2 DESC" + }, + { + "dialect": "snowflake", + "sql": "SELECT model!mladmin", + "formatted": "SELECT model!mladmin" + }, + { + "dialect": "snowflake", + "sql": "SELECT model!PREDICT(1)", + "formatted": "SELECT model!PREDICT(1)" + }, + { + "dialect": "snowflake", + "sql": "SELECT m!PREDICT(INPUT_DATA => {*}) AS p FROM tbl", + "formatted": "SELECT m!PREDICT(INPUT_DATA => {*}) AS p\nFROM tbl" + }, + { + "dialect": "snowflake", + "sql": "SELECT m!PREDICT(INPUT_DATA => {tbl.*}) AS p FROM tbl", + "formatted": "SELECT m!PREDICT(INPUT_DATA => {tbl.*}) AS p\nFROM tbl" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM TABLE(model_trained_with_labeled_data!DETECT_ANOMALIES(INPUT_DATA => TABLE(view_with_data_to_analyze), TIMESTAMP_COLNAME => 'date', TARGET_COLNAME => 'sales', CONFIG_OBJECT => OBJECT_CONSTRUCT('prediction_interval', 0.99)))", + "formatted": "SELECT *\nFROM TABLE(model_trained_with_labeled_data!DETECT_ANOMALIES(\n INPUT_DATA => TABLE(view_with_data_to_analyze),\n TIMESTAMP_COLNAME => 'date',\n TARGET_COLNAME => 'sales',\n CONFIG_OBJECT => OBJECT_CONSTRUCT('prediction_interval', 0.99)\n))" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM TABLE(GENERATOR(ROWCOUNT => 10))", + "formatted": "SELECT 1\nFROM TABLE(GENERATOR(ROWCOUNT => 10))" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM TABLE(GENERATOR(TIMELIMIT => 5))", + "formatted": "SELECT 1\nFROM TABLE(GENERATOR(TIMELIMIT => 5))" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM TABLE(GENERATOR(ROWCOUNT => 10, TIMELIMIT => 5))", + "formatted": "SELECT 1\nFROM TABLE(GENERATOR(ROWCOUNT => 10, TIMELIMIT => 5))" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM TABLE(GENERATOR(10))", + "formatted": "SELECT 1\nFROM TABLE(GENERATOR(10))" + }, + { + "dialect": "snowflake", + "sql": "SELECT 1 FROM TABLE(GENERATOR(10, 5))", + "formatted": "SELECT 1\nFROM TABLE(GENERATOR(10, 5))" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM a CROSS DIRECTED JOIN b USING (id)", + "formatted": "SELECT *\nFROM a\nCROSS DIRECTED JOIN b\n USING (id)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM a INNER DIRECTED JOIN b USING (id)", + "formatted": "SELECT *\nFROM a\nINNER DIRECTED JOIN b\n USING (id)" + }, + { + "dialect": "snowflake", + "sql": "SELECT * FROM a NATURAL INNER DIRECTED JOIN b USING (id)", + "formatted": "SELECT *\nFROM a\nNATURAL INNER DIRECTED JOIN b\n USING (id)" + }, + { + "dialect": "snowflake", + "sql": "SELECT TRY_PARSE_JSON('{\"x: 1}')", + "formatted": "SELECT TRY_PARSE_JSON('{\"x: 1}')" + }, + { + "dialect": "snowflake", + "sql": "SELECT number", + "formatted": "SELECT number" + }, + { + "dialect": "snowflake", + "sql": "SELECT TO_TIMESTAMP(123.4)", + "formatted": "SELECT TO_TIMESTAMP(123.4)" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_PART(year FROM CAST('2024-04-08' AS DATE))", + "formatted": "SELECT DATE_PART(year FROM CAST('2024-04-08' AS DATE))" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_PART('month' FROM CAST('2024-04-08' AS DATE))", + "formatted": "SELECT DATE_PART('month' FROM CAST('2024-04-08' AS DATE))" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATE_PART(day FROM a)", + "formatted": "SELECT DATE_PART(day FROM a)" + }, + { + "dialect": "snowflake", + "sql": "SELECT SEARCH_IP(col, '192.168.0.0')", + "formatted": "SELECT SEARCH_IP(col, '192.168.0.0')" + }, + { + "dialect": "snowflake", + "sql": "SELECT DATEADD(DAY, -7, DATEADD(t.m, 1, CAST('2023-01-03' AS DATE))) FROM (SELECT 'month' AS m) AS t", + "formatted": "SELECT DATEADD(DAY, -7, DATEADD(t.m, 1, CAST('2023-01-03' AS DATE)))\nFROM (\n SELECT 'month' AS m\n) AS t" + }, + { + "dialect": "snowflake", + "sql": "SELECT FILE_URL FROM DIRECTORY(@mystage) WHERE SIZE > 100000", + "formatted": "SELECT FILE_URL\nFROM DIRECTORY(@mystage)\nWHERE SIZE > 100000" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM a..b", + "formatted": "SELECT *\nFROM a..b" + }, + { + "dialect": "tsql", + "sql": "SELECT TOP (SELECT 1) * FROM t", + "formatted": "SELECT TOP (\n SELECT 1\n)\n *\nFROM t" + }, + { + "dialect": "tsql", + "sql": "SELECT ATN2(x, y)", + "formatted": "SELECT ATN2(x, y)" + }, + { + "dialect": "tsql", + "sql": "SELECT EXP(1)", + "formatted": "SELECT EXP(1)" + }, + { + "dialect": "tsql", + "sql": "SELECT SYSDATETIMEOFFSET()", + "formatted": "SELECT SYSDATETIMEOFFSET()" + }, + { + "dialect": "tsql", + "sql": "SELECT COMPRESS('Hello World')", + "formatted": "SELECT COMPRESS('Hello World')" + }, + { + "dialect": "tsql", + "sql": "SELECT TRIM(' test ') AS Result", + "formatted": "SELECT TRIM(' test ') AS Result" + }, + { + "dialect": "tsql", + "sql": "SELECT TRIM('.,! ' FROM ' # test .') AS Result", + "formatted": "SELECT TRIM('.,! ' FROM ' # test .') AS Result" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT)", + "formatted": "SELECT *\nFROM t TABLESAMPLE (10 PERCENT)" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t TABLESAMPLE (20 ROWS)", + "formatted": "SELECT *\nFROM t TABLESAMPLE (20 ROWS)" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t TABLESAMPLE (10 PERCENT) REPEATABLE (123)", + "formatted": "SELECT *\nFROM t TABLESAMPLE (10 PERCENT) REPEATABLE (123)" + }, + { + "dialect": "tsql", + "sql": "SELECT CONCAT(column1, column2)", + "formatted": "SELECT CONCAT(column1, column2)" + }, + { + "dialect": "tsql", + "sql": "SELECT TestSpecialChar.Test# FROM TestSpecialChar", + "formatted": "SELECT TestSpecialChar.Test#\nFROM TestSpecialChar" + }, + { + "dialect": "tsql", + "sql": "SELECT TestSpecialChar.Test@ FROM TestSpecialChar", + "formatted": "SELECT TestSpecialChar.Test@\nFROM TestSpecialChar" + }, + { + "dialect": "tsql", + "sql": "SELECT TestSpecialChar.Test$ FROM TestSpecialChar", + "formatted": "SELECT TestSpecialChar.Test$\nFROM TestSpecialChar" + }, + { + "dialect": "tsql", + "sql": "SELECT TestSpecialChar.Test_ FROM TestSpecialChar", + "formatted": "SELECT TestSpecialChar.Test_\nFROM TestSpecialChar" + }, + { + "dialect": "tsql", + "sql": "SELECT TOP (2 + 1) 1", + "formatted": "SELECT TOP (2 + 1) 1" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t WHERE NOT c", + "formatted": "SELECT *\nFROM t\nWHERE NOT c" + }, + { + "dialect": "tsql", + "sql": "WITH t1 AS (SELECT 1 AS a), t2 AS (SELECT 1 AS a) SELECT TOP 10 a FROM t1 UNION ALL SELECT TOP 10 a FROM t2", + "formatted": "WITH t1 AS (\n SELECT 1 AS a\n),\n\nt2 AS (\n SELECT 1 AS a\n)\n\nSELECT TOP 10 a\nFROM t1\nUNION ALL\nSELECT TOP 10 a\nFROM t2" + }, + { + "dialect": "tsql", + "sql": "SELECT TOP 10 s.RECORDID, n.c.VALUE('(/*:FORM_ROOT/*:SOME_TAG)[1]', 'float') AS SOME_TAG_VALUE FROM source_table.dbo.source_data AS s(nolock) CROSS APPLY FormContent.nodes('/*:FORM_ROOT') AS N(C)", + "formatted": "SELECT TOP 10\n s.RECORDID,\n n.c.VALUE('(/*:FORM_ROOT/*:SOME_TAG)[1]', 'float') AS SOME_TAG_VALUE\nFROM source_table.dbo.source_data AS s(nolock)\nCROSS APPLY FormContent.nodes('/*:FORM_ROOT') AS N(C)" + }, + { + "dialect": "tsql", + "sql": "WITH t1 AS (SELECT 1 AS a), t2 AS (SELECT 1 AS a) SELECT TOP 10 a FROM t1 UNION ALL SELECT TOP 10 a FROM t2 ORDER BY a DESC", + "formatted": "WITH t1 AS (\n SELECT 1 AS a\n),\n\nt2 AS (\n SELECT 1 AS a\n)\n\nSELECT TOP 10 a\nFROM t1\nUNION ALL\nSELECT TOP 10 a\nFROM t2\nORDER BY a DESC" + }, + { + "dialect": "tsql", + "sql": "WITH t1 AS (SELECT 1 AS a), t2 AS (SELECT 1 AS a) SELECT COUNT(*) FROM (SELECT TOP 10 a FROM t1 UNION ALL SELECT TOP 10 a FROM t2 ORDER BY a DESC) AS t", + "formatted": "WITH t1 AS (\n SELECT 1 AS a\n),\n\nt2 AS (\n SELECT 1 AS a\n)\n\nSELECT COUNT(*)\nFROM (\n SELECT TOP 10 a\n FROM t1\n UNION ALL\n SELECT TOP 10 a\n FROM t2\nORDER BY a DESC\n) AS t" + }, + { + "dialect": "tsql", + "sql": "SELECT 1 AS \"[x]\"", + "formatted": "SELECT 1 AS \"[x]\"" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t WITH (TABLOCK, INDEX(myindex))", + "formatted": "SELECT *\nFROM t WITH (TABLOCK, INDEX(myindex))" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t WITH (NOWAIT)", + "formatted": "SELECT *\nFROM t WITH (NOWAIT)" + }, + { + "dialect": "tsql", + "sql": "SELECT CASE WHEN a > 1 THEN b END", + "formatted": "SELECT CASE WHEN a > 1 THEN b END" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM taxi ORDER BY 1 OFFSET 0 ROWS FETCH NEXT 3 ROWS ONLY", + "formatted": "SELECT *\nFROM taxi\nORDER BY\n 1\nOFFSET 0 ROWS\nFETCH NEXT 3 ROWS ONLY" + }, + { + "dialect": "tsql", + "sql": "SELECT Employee_ID, Department_ID FROM @MyTableVar", + "formatted": "SELECT\n Employee_ID,\n Department_ID\nFROM @MyTableVar" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM #foo", + "formatted": "SELECT *\nFROM #foo" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM ##foo", + "formatted": "SELECT *\nFROM ##foo" + }, + { + "dialect": "tsql", + "sql": "SELECT a = 1", + "formatted": "SELECT a = 1" + }, + { + "dialect": "tsql", + "sql": "SELECT a = 1 UNION ALL SELECT a = b", + "formatted": "SELECT a = 1\nUNION ALL\nSELECT a = b" + }, + { + "dialect": "tsql", + "sql": "SELECT x FROM @MyTableVar AS m JOIN Employee ON m.EmployeeID = Employee.EmployeeID", + "formatted": "SELECT x\nFROM @MyTableVar AS m\nJOIN Employee\n ON m.EmployeeID = Employee.EmployeeID" + }, + { + "dialect": "tsql", + "sql": "SELECT DISTINCT DepartmentName, PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY BaseRate) OVER (PARTITION BY DepartmentName) AS MedianCont FROM dbo.DimEmployee", + "formatted": "SELECT DISTINCT\n DepartmentName,\n PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY BaseRate) OVER (PARTITION BY DepartmentName) AS MedianCont\nFROM dbo.DimEmployee" + }, + { + "dialect": "tsql", + "sql": "SELECT \"x\".\"y\" FROM foo", + "formatted": "SELECT \"x\".\"y\"\nFROM foo" + }, + { + "dialect": "tsql", + "sql": "SELECT val FROM (VALUES ((TRUE), (FALSE), (NULL))) AS t(val)", + "formatted": "SELECT val\nFROM (\n VALUES\n ((TRUE\n), (FALSE\n), (\n NULL\n))\n) AS t(val)" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t FOR XML PATH, BINARY BASE64, ELEMENTS XSINIL", + "formatted": "SELECT *\nFROM t\nFOR XML\n PATH,\n BINARY BASE64,\n ELEMENTS XSINIL" + }, + { + "dialect": "tsql", + "sql": "SELECT (SELECT TOP 5 a, b FROM t ORDER BY b DESC FOR JSON PATH) AS j FROM x", + "formatted": "SELECT\n (\n SELECT TOP 5\n a,\n b\n FROM t\n ORDER BY\n b DESC\n FOR JSON\n PATH\n ) AS j\nFROM x" + }, + { + "dialect": "tsql", + "sql": "SELECT j FROM (SELECT a AS j FROM t FOR JSON PATH) AS x", + "formatted": "SELECT j\nFROM (\n SELECT a AS j\n FROM t\n FOR JSON\n PATH\n) AS x" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t FOR JSON PATH, ROOT('Root'), INCLUDE_NULL_VALUES", + "formatted": "SELECT *\nFROM t\nFOR JSON\n PATH,\n ROOT('Root'),\n INCLUDE_NULL_VALUES" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM t FOR BROWSE", + "formatted": "SELECT *\nFROM t FOR BROWSE" + }, + { + "dialect": "tsql", + "sql": "SELECT CAST(SUBSTRING('ABCD~1234', CHARINDEX('~', 'ABCD~1234') + 1, LEN('ABCD~1234')) AS BIGINT)", + "formatted": "SELECT CAST(SUBSTRING('ABCD~1234', CHARINDEX('~', 'ABCD~1234') + 1, LEN('ABCD~1234')) AS BIGINT)" + }, + { + "dialect": "tsql", + "sql": "SELECT DATEPART(\"dd\", x)", + "formatted": "SELECT DATEPART(\"dd\", x)" + }, + { + "dialect": "tsql", + "sql": "SELECT DATEADD(YEAR, 1, '2017/08/25')", + "formatted": "SELECT DATEADD(YEAR, 1, '2017/08/25')" + }, + { + "dialect": "tsql", + "sql": "SELECT DATEDIFF(HOUR, 1.5, '2021-01-01')", + "formatted": "SELECT DATEDIFF(HOUR, 1.5, '2021-01-01')" + }, + { + "dialect": "tsql", + "sql": "SELECT DATEDIFF_BIG(HOUR, 1.5, '2021-01-01')", + "formatted": "SELECT DATEDIFF_BIG(HOUR, 1.5, '2021-01-01')" + }, + { + "dialect": "tsql", + "sql": "SELECT TOP 10 PERCENT", + "formatted": "SELECT TOP 10 PERCENT" + }, + { + "dialect": "tsql", + "sql": "SELECT TOP 10 PERCENT WITH TIES", + "formatted": "SELECT TOP 10 PERCENT WITH TIES" + }, + { + "dialect": "tsql", + "sql": "SELECT FORMAT(foo, 'dddd', 'de-CH')", + "formatted": "SELECT FORMAT(foo, 'dddd', 'de-CH')" + }, + { + "dialect": "tsql", + "sql": "SELECT FORMAT(EndOfDayRate, 'N', 'en-us')", + "formatted": "SELECT FORMAT(EndOfDayRate, 'N', 'en-us')" + }, + { + "dialect": "tsql", + "sql": "SELECT FORMAT('01-01-1991', 'd.mm.yyyy')", + "formatted": "SELECT FORMAT('01-01-1991', 'd.mm.yyyy')" + }, + { + "dialect": "tsql", + "sql": "SELECT FORMAT(12345, '###.###.###')", + "formatted": "SELECT FORMAT(12345, '###.###.###')" + }, + { + "dialect": "tsql", + "sql": "SELECT FORMAT(1234567, 'f')", + "formatted": "SELECT FORMAT(1234567, 'f')" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo'", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME AS OF 'foo'" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME AS OF 'foo' AS alias", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME AS OF 'foo' AS alias" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME FROM c TO d", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME FROM c TO d" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME BETWEEN c AND d", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME BETWEEN c AND d" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME CONTAINED IN (c, d)", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME CONTAINED IN (c, d)" + }, + { + "dialect": "tsql", + "sql": "SELECT [x] FROM [a].[b] FOR SYSTEM_TIME ALL AS alias", + "formatted": "SELECT [x]\nFROM [a].[b] FOR SYSTEM_TIME ALL AS alias" + }, + { + "dialect": "tsql", + "sql": "SELECT x FROM a INNER LOOP JOIN b ON b.id = a.id", + "formatted": "SELECT x\nFROM a\nINNER LOOP JOIN b\n ON b.id = a.id" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM OPENJSON(@json)", + "formatted": "SELECT *\nFROM OPENJSON(@json)" + }, + { + "dialect": "tsql", + "sql": "WITH t AS (SELECT 1) SELECT * FROM t", + "formatted": "WITH t AS (\n SELECT 1\n)\n\nSELECT *\nFROM t" + }, + { + "dialect": "tsql", + "sql": "WITH t AS (SELECT \"c\") SELECT * FROM t", + "formatted": "WITH t AS (\n SELECT \"c\"\n)\n\nSELECT *\nFROM t" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM (SELECT 1) AS subq", + "formatted": "SELECT *\nFROM (\n SELECT 1\n) AS subq" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM (SELECT \"c\") AS subq", + "formatted": "SELECT *\nFROM (\n SELECT \"c\"\n) AS subq" + }, + { + "dialect": "tsql", + "sql": "SELECT NEXT VALUE FOR db.schema.sequence_name OVER (ORDER BY foo), col", + "formatted": "SELECT\n NEXT VALUE FOR db.schema.sequence_name OVER (ORDER BY foo),\n col" + }, + { + "dialect": "tsql", + "sql": "SELECT COUNT(1) FROM x", + "formatted": "SELECT COUNT(1)\nFROM x" + }, + { + "dialect": "tsql", + "sql": "SELECT COUNT_BIG(1) FROM x", + "formatted": "SELECT COUNT_BIG(1)\nFROM x" + }, + { + "dialect": "tsql", + "sql": "SELECT 1; SELECT 2", + "formatted": "SELECT 1;\nSELECT 2" + }, + { + "dialect": "tsql", + "sql": "SELECT go", + "formatted": "SELECT go" + }, + { + "dialect": "tsql", + "sql": "SELECT 1 WHERE EXISTS(SELECT 1)", + "formatted": "SELECT 1\nWHERE\n EXISTS(\n SELECT 1\n )" + }, + { + "dialect": "tsql", + "sql": "SELECT a = 1", + "formatted": "SELECT a = 1" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM [#temp_table]", + "formatted": "SELECT *\nFROM [#temp_table]" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM [##temp_table]", + "formatted": "SELECT *\nFROM [##temp_table]" + }, + { + "dialect": "tsql", + "sql": "SELECT * FROM @x", + "formatted": "SELECT *\nFROM @x" + } +] diff --git a/crates/sqlclean/src/sql_lint/tests/helpers.rs b/crates/sqlclean/src/sql_lint/tests/helpers.rs new file mode 100644 index 0000000..bb4c1b9 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/helpers.rs @@ -0,0 +1,304 @@ +use std::str::FromStr; + +use polyglot_sql::{Dialect, DialectType}; +use serde_json::{Map, Value, json}; + +use crate::sql_lint::_helpers::formatter::format_requests; +use crate::sql_lint::main::engine::lint_json; +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::models::FormatRequest; +use crate::sql_lint::tests::test_types::CrossJoinTestCase; +use crate::sql_tokens::constants::CALL_SYNTAX_FUNCTIONS; +use crate::sql_tokens::main::builtin_function_names::builtin_function_names; +use crate::sql_tokens::main::canonical_tokens::canonical_tokens; +use crate::sql_tokens::main::query_fingerprint::query_fingerprint; + +/// The dependency sentinel the cross join cases name `order_policy`. +pub(crate) const POLICY_SENTINEL: &str = "__sqb_lint_7__"; + +/// Format a JSON array of format requests in parallel, as a JSON array of responses. +pub(crate) fn format_batch_json(request_json: &str) -> Result { + let requests: Vec = + serde_json::from_str(request_json).map_err(|error| error.to_string())?; + serde_json::to_string(&format_requests(requests)).map_err(|error| error.to_string()) +} + +pub(crate) fn nested_function_sql(depth: usize) -> String { + format!("SELECT {}1{}", "F(".repeat(depth), ")".repeat(depth)) +} + +pub(crate) fn diagnostics(sql: &str) -> Result, String> { + let response = lint_json( + &json!({ + "version": 1, + "sql": sql, + "dialect": "snowflake" + }) + .to_string(), + )?; + diagnostic_values(&response) +} + +pub(crate) fn diagnostics_for_rules(sql: &str, rules: &[&str]) -> Result, String> { + diagnostics_for_dialect(sql, "snowflake", rules) +} + +pub(crate) fn diagnostics_for_dialect( + sql: &str, + dialect: &str, + rules: &[&str], +) -> Result, String> { + let request = json!({ + "version": 1, + "sql": sql, + "dialect": dialect, + "enabled_rules": rules, + }); + let response = lint_json(&request.to_string())?; + diagnostic_values(&response) +} + +fn diagnostic_values(response: &str) -> Result, String> { + let payload: Value = serde_json::from_str(response).map_err(|error| error.to_string())?; + payload["diagnostics"] + .as_array() + .cloned() + .ok_or_else(|| "diagnostics should be an array".to_string()) +} + +/// A narrow width so that wrapping is exercised on most corpus queries. +const CORPUS_LINE_WIDTH: usize = 40; + +/// Format every corpus query of one dialect and return the formatted and refused counts. +pub(crate) fn check_dialect_corpus( + corpus: &[Value], + dialect: &str, +) -> Result<(usize, usize), String> { + let mut outcomes: Vec = Vec::new(); + for entry in corpus.iter().filter(|entry| entry["dialect"] == dialect) { + let sql = entry["sql"].as_str().unwrap_or_default(); + let response = format_once(sql, dialect, None)?; + let formatted = response["formatted"] == true; + let actual = response + .get("sql") + .filter(|_| formatted) + .cloned() + .unwrap_or(Value::Null); + let expected = entry["formatted"].as_str().map_or(Value::Null, Value::from); + assert_eq!(actual, expected, "{dialect}: {sql}"); + let printed = actual.as_str().unwrap_or(sql); + assert!( + only_keyword_case_differs(sql, printed, dialect)?, + "{dialect} fingerprint changed beyond keyword case: {sql}" + ); + let previous_layout: Option = entry["previously_formatted"] + .as_str() + .map(|previous| relaid_previous_layout(previous, dialect)) + .transpose()?; + let layouts = [ + entry["formatted"].as_str().map(str::to_string), + previous_layout, + ]; + for stable in layouts.iter().flatten() { + let stable = stable.as_str(); + let again = format_once(stable, dialect, None)?; + assert_eq!(again["sql"], stable, "{dialect} layout drifted: {stable}"); + assert_eq!( + again["changed"], false, + "{dialect} layout drifted: {stable}" + ); + assert_eq!( + query_fingerprint(again["sql"].as_str().unwrap_or_default(), dialect)?, + query_fingerprint(stable, dialect)?, + "{dialect} formatting changed the fingerprint of formatted SQL: {stable}" + ); + } + let wrapped = format_once(sql, dialect, Some(CORPUS_LINE_WIDTH))?; + assert_eq!(wrapped["formatted"], formatted, "{dialect} wrapped: {sql}"); + let wrapped_sql = wrapped["sql"].as_str().unwrap_or_default(); + let rewrapped = format_once(wrapped_sql, dialect, Some(CORPUS_LINE_WIDTH))?; + assert_eq!( + rewrapped["sql"], wrapped_sql, + "{dialect} wrapping drifted: {sql}" + ); + outcomes.push(formatted); + } + let formatted = outcomes.iter().filter(|outcome| **outcome).count(); + Ok((formatted, outcomes.len() - formatted)) +} + +/// Whether `printed` has the fingerprint tokens of `sql` except for words upper-cased by format. +fn only_keyword_case_differs(sql: &str, printed: &str, dialect: &str) -> Result { + let parser = Dialect::get(DialectType::from_str(dialect).map_err(|error| error.to_string())?); + let left_tokens = parser.tokenize(sql).map_err(|error| error.to_string())?; + let right_tokens = parser + .tokenize(printed) + .map_err(|error| error.to_string())?; + let left = canonical_tokens(sql, &left_tokens, &parser)?; + let right = canonical_tokens(printed, &right_tokens, &parser)?; + Ok(left.len() == right.len() + && left.iter().zip(&right).all(|(before, after)| { + before == after + || (before.text.eq_ignore_ascii_case(&after.text) + && after.text == after.text.to_ascii_uppercase()) + })) +} + +/// Re-format a layout of an earlier formatter release, which must keep its fingerprint. +fn relaid_previous_layout(previous: &str, dialect: &str) -> Result { + let relaid = format_once(previous, dialect, None)?; + let sql = relaid["sql"].as_str().unwrap_or_default().to_string(); + assert_eq!( + query_fingerprint(&sql, dialect)?, + query_fingerprint(previous, dialect)?, + "{dialect} formatting changed the fingerprint of a previous layout: {previous}" + ); + Ok(sql) +} + +pub(crate) fn format_at_width( + sql: &str, + dialect: &str, + line_width: usize, +) -> Result { + format_once(sql, dialect, Some(line_width)) +} + +fn format_once(sql: &str, dialect: &str, line_width: Option) -> Result { + let request = json!({"version": 1, "sql": sql, "dialect": dialect, "line_width": line_width}); + let response = format_json(&request.to_string())?; + serde_json::from_str(&response).map_err(|error| error.to_string()) +} + +/// Call names the formatter treats as built-ins in `dialect`, plus keyword-like call names. +const KEYWORD_LIKE_CALLS: [&str; 11] = [ + "if", + "iff", + "left", + "right", + "replace", + "insert", + "format", + "filter", + "date", + "time", + "timestamp", +]; + +/// Return names no 2/1/0-argument call could format, and calls printed with misplaced spaces. +pub(crate) fn function_call_spacing( + dialect: &str, + line_width: usize, + argument: &str, +) -> Result<(Vec, Vec), String> { + let dialect_type = DialectType::from_str(dialect).map_err(|error| error.to_string())?; + let mut names: Vec = builtin_function_names(dialect_type) + .into_iter() + .chain(KEYWORD_LIKE_CALLS.iter().map(|name| (*name).to_string())) + .filter(|name| name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')) + .filter(|name| !CALL_SYNTAX_FUNCTIONS.contains(&name.to_ascii_uppercase().as_str())) + .collect(); + names.sort_unstable(); + names.dedup(); + let mut misplaced: Vec = Vec::new(); + for arguments in [ + format!("{argument}, 1"), + argument.to_string(), + String::new(), + ] { + let responses = format_calls(&names, &arguments, dialect, line_width)?; + misplaced.extend( + names + .iter() + .zip(&responses) + .filter(|(_, response)| response["formatted"] == true) + .filter(|(name, response)| { + !is_tight_call( + name, + response["sql"].as_str().unwrap_or_default(), + &arguments, + ) + }) + .map(|(name, response)| format!("{name}: {}", response["sql"])), + ); + names = names + .into_iter() + .zip(&responses) + .filter(|(_, response)| response["formatted"] != true) + .map(|(name, _)| name) + .collect(); + } + Ok((names, misplaced)) +} + +fn format_calls( + names: &[String], + arguments: &str, + dialect: &str, + line_width: usize, +) -> Result, String> { + let requests: Vec = names + .iter() + .map(|name| { + json!({ + "version": 1, + "sql": format!("SELECT {name}({arguments}) AS x FROM t"), + "dialect": dialect, + "line_width": line_width, + }) + }) + .collect(); + serde_json::from_str(&format_batch_json(&Value::from(requests).to_string())?) + .map_err(|error| error.to_string()) +} + +/// Whether `sql` prints `name(` in authored or upper case with no space inside the call. +fn is_tight_call(name: &str, sql: &str, arguments: &str) -> bool { + [name.to_string(), name.to_ascii_uppercase()] + .iter() + .any(|printed| { + sql.contains(&format!("{printed}({arguments})")) + || sql.contains(&format!("{printed}(\n")) + }) + && !sql.contains("( ") + && !sql + .to_ascii_uppercase() + .contains(&format!("{} (", name.to_ascii_uppercase())) +} + +/// The `SQC038` messages for one cross join case. +pub(crate) fn cross_join_messages(test_case: &CrossJoinTestCase) -> Result, String> { + cross_join_texts(test_case, "message") +} + +/// The `SQC038` remediations for one cross join case. +pub(crate) fn cross_join_remediations( + test_case: &CrossJoinTestCase, +) -> Result, String> { + cross_join_texts(test_case, "remediation") +} + +fn cross_join_texts(test_case: &CrossJoinTestCase, field: &str) -> Result, String> { + let keys: Map = test_case + .one_row_relations + .iter() + .map(|name| ((*name).to_owned(), json!([[]]))) + .collect(); + let request = json!({ + "version": 1, + "sql": test_case.sql, + "dialect": test_case.dialect, + "enabled_rules": ["SQC038"], + "relation_keys": keys, + "relation_names": { POLICY_SENTINEL: "order_policy" }, + }); + let response: Value = serde_json::from_str(&lint_json(&request.to_string())?) + .map_err(|error| error.to_string())?; + Ok(response["diagnostics"] + .as_array() + .cloned() + .unwrap_or_default() + .iter() + .map(|diagnostic| diagnostic[field].as_str().unwrap_or_default().to_owned()) + .collect()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_authored_tokens.rs b/crates/sqlclean/src/sql_lint/tests/test_authored_tokens.rs new file mode 100644 index 0000000..cb29e9c --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_authored_tokens.rs @@ -0,0 +1,416 @@ +use serde_json::{Value, json}; + +use std::collections::BTreeMap; + +use crate::sql_lint::_helpers::token_layout::{ + KeywordRecase, TokenTexts, tokenize, verify_printed_tokens, +}; +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_generator_respellings_when_formatting_then_authored_spellings_are_kept() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "Snowflake STARTSWITH keeps its authored name", + sql: "select startswith(order_code, 'EU') as is_eu_order from orders", + expected_sql: "SELECT startswith(order_code, 'EU') AS is_eu_order\nFROM orders", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "function synonyms keep their authored names", + sql: "select SUBSTR(order_code, 1, 2) as region, TIMESTAMPDIFF(day, ordered_at, shipped_at) as days_to_ship, TRY_TO_DECIMAL(amount_text, 10, 2) as amount from orders", + expected_sql: "SELECT\n SUBSTR(order_code, 1, 2) AS region,\n TIMESTAMPDIFF(day, ordered_at, shipped_at) AS days_to_ship,\n TRY_TO_DECIMAL(amount_text, 10, 2) AS amount\nFROM orders", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "operator and function spellings stay authored", + sql: "select IFNULL(status, 'open') as status from orders where quantity != 0", + expected_sql: "SELECT IFNULL(status, 'open') AS status\nFROM orders\nWHERE quantity != 0", + expected_changed: true, + }, + ]; + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "snowflake"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_generator_restructuring_when_formatting_then_authored_tokens_are_laid_out() +-> Result<(), String> { + let test_cases = [ + test_types::DialectFormatTestCase { + description: "PostgreSQL comma SUBSTRING arguments are not rewritten to FROM/FOR", + dialect: "postgres", + sql: "select substring(code, 1, 2) as region from orders", + expected_sql: "SELECT SUBSTRING(code, 1, 2) AS region\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "DuckDB adjacent string literals after a quoted-in-oracle alias keep comma spacing", + dialect: "duckdb", + sql: "select sku as replace, 'north' 'east' as region, sku from products", + expected_sql: "SELECT\n sku AS replace,\n 'north' 'east' AS region,\n sku\nFROM products", + }, + test_types::DialectFormatTestCase { + description: "DuckDB adjacent string literals after a plain column keep comma spacing", + dialect: "duckdb", + sql: "select sku, 'north' 'east' as region from products", + expected_sql: "SELECT\n sku,\n 'north' 'east' AS region\nFROM products", + }, + test_types::DialectFormatTestCase { + description: "DuckDB SELECT ALL keeps ALL", + dialect: "duckdb", + sql: "select all order_id from orders", + expected_sql: "SELECT ALL order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "Snowflake SELECT ALL keeps ALL", + dialect: "snowflake", + sql: "select all order_id from orders", + expected_sql: "SELECT ALL order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "PostgreSQL SELECT ALL keeps ALL", + dialect: "postgres", + sql: "select all order_id from orders", + expected_sql: "SELECT ALL order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "BigQuery SELECT ALL keeps ALL", + dialect: "bigquery", + sql: "select all order_id from orders", + expected_sql: "SELECT ALL order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "Databricks SELECT ALL keeps ALL", + dialect: "databricks", + sql: "select all order_id from orders", + expected_sql: "SELECT all order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "T-SQL SELECT ALL keeps ALL", + dialect: "tsql", + sql: "select all order_id from orders", + expected_sql: "SELECT ALL order_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "implicit aliases stay implicit", + dialect: "duckdb", + sql: "select order_id id, amount from orders o", + expected_sql: "SELECT\n order_id id,\n amount\nFROM orders o", + }, + test_types::DialectFormatTestCase { + description: "a trailing select comma and statement terminator are kept", + dialect: "duckdb", + sql: "select order_id, amount, from orders;", + expected_sql: "SELECT\n order_id,\n amount,\nFROM orders;", + }, + test_types::DialectFormatTestCase { + description: "adjacent authored tokens are never split", + dialect: "duckdb", + sql: "select $name as v from orders where status != 1", + expected_sql: "SELECT $name AS v\nFROM orders\nWHERE status != 1", + }, + test_types::DialectFormatTestCase { + description: "BigQuery numeric-prefixed path parts stay joined", + dialect: "bigquery", + sql: "select * from orders.archive.25_", + expected_sql: "SELECT *\nFROM orders.archive.25_", + }, + ]; + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": test_case.dialect}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + let again = format_json( + &json!({"version": 1, "sql": test_case.expected_sql, "dialect": test_case.dialect}) + .to_string(), + )?; + let repeated: Value = serde_json::from_str(&again).map_err(|error| error.to_string())?; + assert_eq!(repeated["changed"], false, "{}", test_case.description); + } + Ok(()) +} + +#[test] +fn given_case_or_line_sensitive_tokens_when_formatting_then_they_are_kept_exactly() +-> Result<(), String> { + let test_cases = [ + test_types::DialectFormatTestCase { + description: "DuckDB keyword-named aliases and qualified names keep their case", + dialect: "duckdb", + sql: "select pos as index, sku as replace, t.view from orders t", + expected_sql: "SELECT\n pos AS index,\n sku AS replace,\n t.view\nFROM orders t", + }, + test_types::DialectFormatTestCase { + description: "BigQuery keyword-named table names keep their case", + dialect: "bigquery", + sql: "select * from inventory.view", + expected_sql: "SELECT *\nFROM inventory.view", + }, + test_types::DialectFormatTestCase { + description: "Snowflake path keys keep their case", + dialect: "snowflake", + sql: "select payload:Customer.Name as name, payload:Date::string as day from orders", + expected_sql: "SELECT\n payload:Customer.Name AS name,\n payload:Date::string AS day\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "user-defined functions keep their case, built-ins are uppercased", + dialect: "bigquery", + sql: "select MyFunc(a), ds.MyUdf(b), coalesce(a, b) from orders", + expected_sql: "SELECT\n MyFunc(a),\n ds.MyUdf(b),\n COALESCE(a, b)\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "DuckDB non-reserved keyword operands keep their case before any operator", + dialect: "duckdb", + sql: "select a, Rows << 1, Rows == 1, Rows ^ 2 from t", + expected_sql: "SELECT\n a,\n Rows << 1,\n Rows == 1,\n Rows ^ 2\nFROM t", + }, + test_types::DialectFormatTestCase { + description: "BigQuery comma-joined keyword-named tables keep their case", + dialect: "bigquery", + sql: "select * from ds.a, View v", + expected_sql: "SELECT *\nFROM ds.a, View v", + }, + test_types::DialectFormatTestCase { + description: "DuckDB adjacent string literals keep their line break", + dialect: "duckdb", + sql: "select 'north'\n'east' as region from orders", + expected_sql: "SELECT\n 'north'\n 'east' AS region\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "PostgreSQL adjacent string literals keep their line break", + dialect: "postgres", + sql: "select 'north'\n 'east' as region from orders", + expected_sql: "SELECT\n 'north'\n 'east' AS region\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "Snowflake double-slash comments are kept", + dialect: "snowflake", + sql: "select order_id, // keep this note\n customer_id from orders", + expected_sql: "SELECT\n order_id, // keep this note\n customer_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "BigQuery hash comments are kept", + dialect: "bigquery", + sql: "select order_id, # keep this note\n customer_id from orders", + expected_sql: "SELECT\n order_id, # keep this note\n customer_id\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "T-SQL temporary table hashes are not comments", + dialect: "tsql", + sql: "select #orders.order_id from #orders", + expected_sql: "SELECT #orders.order_id\nFROM #orders", + }, + test_types::DialectFormatTestCase { + description: "DuckDB integer division and slashes inside strings are not comments", + dialect: "duckdb", + sql: "select 7 // 2 as half, 'https://example.com/a' as url from orders", + expected_sql: "SELECT\n 7 // 2 AS half,\n 'https://example.com/a' AS url\nFROM orders", + }, + test_types::DialectFormatTestCase { + description: "Snowflake double slashes inside strings are not comments", + dialect: "snowflake", + sql: "select 'https://example.com//a' as url from orders", + expected_sql: "SELECT 'https://example.com//a' AS url\nFROM orders", + }, + ]; + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": test_case.dialect}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + let again = format_json( + &json!({"version": 1, "sql": test_case.expected_sql, "dialect": test_case.dialect}) + .to_string(), + )?; + let repeated: Value = serde_json::from_str(&again).map_err(|error| error.to_string())?; + assert_eq!(repeated["changed"], false, "{}", test_case.description); + } + Ok(()) +} + +#[test] +fn given_formatted_candidates_when_checking_token_invariant_then_only_layout_may_differ() { + let test_cases = [ + test_types::TokenInvariantTestCase { + description: "whitespace and word case may change", + before: "select a,b from orders -- note", + after: "SELECT\n a,\n b\nFROM orders -- note", + recases: &[], + expected_error: None, + }, + test_types::TokenInvariantTestCase { + description: "blank lines between CTEs keep comments on their tokens", + before: "WITH orders AS (SELECT 1), -- note\n-- paid only\npaid AS (SELECT 2) SELECT * FROM paid", + after: "WITH orders AS (\n SELECT 1\n), -- note\n\n-- paid only\npaid AS (\n SELECT 2\n)\n\nSELECT *\nFROM paid", + recases: &[], + expected_error: None, + }, + test_types::TokenInvariantTestCase { + description: "a comment moved across a CTE blank line is refused", + before: "WITH orders AS (SELECT 1), -- note\npaid AS (SELECT 2) SELECT * FROM paid", + after: "WITH orders AS (\n SELECT 1\n)\n-- note\n,\n\npaid AS (\n SELECT 2\n)\n\nSELECT *\nFROM paid", + recases: &[], + expected_error: Some("native formatter could not preserve comment token attachments"), + }, + test_types::TokenInvariantTestCase { + description: "a renamed function is refused", + before: "select startswith(code, 'EU') from orders", + after: "SELECT STARTS_WITH(code, 'EU') FROM orders", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "an added alias keyword is refused", + before: "select a b from orders", + after: "SELECT a AS b FROM orders", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "string literal whitespace is refused", + before: "select 'a b' from orders", + after: "SELECT 'a b' FROM orders", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "a recorded keyword recase from the layout guide is allowed", + before: "select a from orders order by a nulls last", + after: "SELECT a FROM orders ORDER BY a NULLS LAST", + recases: &[(7, "NULLS", true), (8, "LAST", true)], + expected_error: None, + }, + test_types::TokenInvariantTestCase { + description: "an unrecorded non-reserved keyword recase is refused", + before: "select a from orders order by a nulls last", + after: "SELECT a FROM orders ORDER BY a NULLS LAST", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "a recase whose guide token is not a keyword is refused", + before: "select a from orders order by a nulls last", + after: "SELECT a FROM orders ORDER BY a NULLS LAST", + recases: &[(7, "NULLS", false), (8, "LAST", false)], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "an identifier case change is refused", + before: "select a from orders", + after: "SELECT a FROM Orders", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "a path key case change is refused", + before: "select payload:customerId from orders", + after: "SELECT payload:CUSTOMERID FROM orders", + recases: &[], + expected_error: Some( + "native formatter would change authored SQL tokens, not only layout", + ), + }, + test_types::TokenInvariantTestCase { + description: "a built-in function may be recased", + before: "select coalesce(a, 0) from orders", + after: "SELECT COALESCE(a, 0) FROM orders", + recases: &[], + expected_error: None, + }, + test_types::TokenInvariantTestCase { + description: "joining adjacent string literals across a line break is refused", + before: "select 'a'\n'b' from orders", + after: "SELECT 'a' 'b' FROM orders", + recases: &[], + expected_error: Some( + "native formatter would join adjacent string literals across a line break", + ), + }, + test_types::TokenInvariantTestCase { + description: "a dropped double-slash comment is refused", + before: "select a // note\nfrom orders", + after: "SELECT a FROM orders", + recases: &[], + expected_error: Some("native formatter could not preserve comment token attachments"), + }, + test_types::TokenInvariantTestCase { + description: "a comment moved to another token is refused", + before: "select a, -- note\n b from orders", + after: "SELECT a, b -- note\nFROM orders", + recases: &[], + expected_error: Some("native formatter could not preserve comment token attachments"), + }, + ]; + let dialect = polyglot_sql::Dialect::get(polyglot_sql::DialectType::Snowflake); + for test_case in test_cases { + let recases: BTreeMap = test_case + .recases + .iter() + .map(|(index, generated, keyword)| { + ( + *index, + KeywordRecase { + generated: (*generated).to_string(), + keyword: *keyword, + }, + ) + }) + .collect(); + let before_tokens = tokenize(test_case.before, &dialect).expect("before tokenizes"); + let after_tokens = tokenize(test_case.after, &dialect).expect("after tokenizes"); + let before_texts = + TokenTexts::read(test_case.before, &before_tokens, &dialect).expect("before reads"); + let actual = verify_printed_tokens( + (test_case.before, &before_tokens, &before_texts), + (test_case.after, &after_tokens), + &dialect, + &recases, + ); + assert_eq!( + actual.err().as_deref(), + test_case.expected_error, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_batch_lint.rs b/crates/sqlclean/src/sql_lint/tests/test_batch_lint.rs new file mode 100644 index 0000000..5e4bf9c --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_batch_lint.rs @@ -0,0 +1,43 @@ +use crate::sql_lint::main::batch_engine::lint_batch_json; +use crate::sql_lint::main::engine::lint_json; +use crate::sql_lint::tests::test_types::BatchLintTestCase; +use serde_json::{Value, json}; + +#[test] +fn given_mixed_lint_inputs_when_batching_then_preserves_order_errors_and_findings() { + let test_cases = [BatchLintTestCase { + description: "mixed valid and invalid SQL retains standalone evidence", + queries: &[ + "SELECT 1", + "SELECT FROM", + "SELECT order_id FROM orders WHERE order_id = NULL", + ], + expected_responses: 3, + }]; + for test_case in test_cases { + let requests: Vec = test_case + .queries + .iter() + .map(|sql| json!({"version": 1, "sql": sql, "dialect": "duckdb"})) + .collect(); + let expected: Vec = requests + .iter() + .map(|request| { + serde_json::to_value(lint_json(&request.to_string()).map(|response| { + serde_json::from_str::(&response).expect("valid response") + })) + .expect("serializable result") + }) + .collect(); + let response = lint_batch_json(&serde_json::to_string(&requests).expect("valid requests")) + .expect("valid batch"); + let actual: Vec = serde_json::from_str(&response).expect("valid response"); + assert_eq!( + actual.len(), + test_case.expected_responses, + "{}", + test_case.description + ); + assert_eq!(actual, expected, "{}", test_case.description); + } +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_comment_formatter.rs b/crates/sqlclean/src/sql_lint/tests/test_comment_formatter.rs new file mode 100644 index 0000000..adc81ce --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_comment_formatter.rs @@ -0,0 +1,95 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_leading_cte_comments_when_formatting_then_comments_stay_above_their_cte() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "leading comment before a middle CTE stays above it", + sql: "WITH\nupstream AS (\n SELECT * FROM orders\n),\n\n-- Explains the next CTE: line one,\n-- line two.\ndistinct_rows AS (\n SELECT order_id FROM upstream GROUP BY ALL\n)\n\nSELECT order_id FROM distinct_rows", + expected_sql: "WITH upstream AS (\n SELECT *\n FROM orders\n),\n\n-- Explains the next CTE: line one,\n-- line two.\ndistinct_rows AS (\n SELECT order_id\n FROM upstream\n GROUP BY ALL\n)\n\nSELECT order_id\nFROM distinct_rows", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "formatted leading CTE comment is idempotent", + sql: "WITH upstream AS (\n SELECT *\n FROM orders\n),\n\n-- Explains the next CTE: line one,\n-- line two.\ndistinct_rows AS (\n SELECT order_id\n FROM upstream\n GROUP BY ALL\n)\n\nSELECT order_id\nFROM distinct_rows", + expected_sql: "WITH upstream AS (\n SELECT *\n FROM orders\n),\n\n-- Explains the next CTE: line one,\n-- line two.\ndistinct_rows AS (\n SELECT order_id\n FROM upstream\n GROUP BY ALL\n)\n\nSELECT order_id\nFROM distinct_rows", + expected_changed: false, + }, + test_types::FormatTestCase { + description: "leading comment before the first CTE stays above it", + sql: "WITH\n-- Explains the first CTE.\nupstream AS (SELECT * FROM orders)\nSELECT order_id FROM upstream", + expected_sql: "WITH\n-- Explains the first CTE.\nupstream AS (\n SELECT *\n FROM orders\n)\n\nSELECT order_id\nFROM upstream", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "leading comment before the last of three CTEs stays above it", + sql: "WITH a AS (SELECT 1 AS x),\nb AS (SELECT x FROM a),\n /* Explains the last CTE. */\nc AS (SELECT x FROM b)\nSELECT x FROM c", + expected_sql: "WITH a AS (\n SELECT 1 AS x\n),\n\nb AS (\n SELECT x\n FROM a\n),\n\n/* Explains the last CTE. */\nc AS (\n SELECT x\n FROM b\n)\n\nSELECT x\nFROM c", + expected_changed: true, + }, + ]; + + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "snowflake"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_trailing_comments_after_the_last_token_when_formatting_then_no_newline_is_added() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "a trailing line comment ends the output without a newline", + sql: "SELECT 1 AS order_id\nFROM orders -- latest orders", + expected_sql: "SELECT 1 AS order_id\nFROM orders -- latest orders", + expected_changed: false, + }, + test_types::FormatTestCase { + description: "line comments after the last token keep no trailing newline", + sql: "select 1 as order_id from orders\n\n-- first note\n\n-- second note", + expected_sql: "SELECT 1 AS order_id\nFROM orders -- first note\n -- second note", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "a trailing block comment ends the output", + sql: "SELECT 1 AS order_id\nFROM orders /* latest orders */", + expected_sql: "SELECT 1 AS order_id\nFROM orders /* latest orders */", + expected_changed: false, + }, + ]; + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "duckdb"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_cross_joins.rs b/crates/sqlclean/src/sql_lint/tests/test_cross_joins.rs new file mode 100644 index 0000000..eb0645d --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_cross_joins.rs @@ -0,0 +1,286 @@ +use crate::sql_lint::main::cross_join_subject::cross_join_subject; +use crate::sql_lint::tests::helpers::{ + POLICY_SENTINEL, cross_join_messages, cross_join_remediations, +}; +use crate::sql_lint::tests::test_types::{CrossJoinSubjectTestCase, CrossJoinTestCase}; + +const GENERIC_MESSAGE: &str = "CROSS JOIN creates an explicit cartesian product"; +const UNNAMED_MESSAGE: &str = "CROSS JOIN creates an explicit cartesian product with an unnamed relation; only a file-wide exception (rule and path) can cover it"; + +#[test] +fn given_cross_joins_when_one_side_is_provably_one_row_then_only_multiplying_joins_are_reported() +-> Result<(), String> { + let test_cases = [ + CrossJoinTestCase { + description: "two unknown relations multiply rows", + sql: "SELECT o.order_id FROM orders AS o CROSS JOIN customers AS c", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `customers` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a one-row seed on the right", + sql: "SELECT o.order_id FROM orders AS o CROSS JOIN __sqb_lint_7__ AS p", + dialect: "duckdb", + one_row_relations: &[POLICY_SENTINEL], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "a seed with several rows names its relation", + sql: "SELECT o.order_id FROM orders AS o CROSS JOIN __sqb_lint_7__ AS p", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `order_policy` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a one-row seed on the left of the first join", + sql: "SELECT o.order_id FROM __sqb_lint_7__ AS p CROSS JOIN orders AS o", + dialect: "duckdb", + one_row_relations: &[POLICY_SENTINEL], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "an aggregate without GROUP BY carried through a filtering CTE", + sql: "WITH totals AS (SELECT COUNT(*) AS order_count FROM orders), \ + kept AS (SELECT order_count FROM totals WHERE order_count > 0) \ + SELECT o.order_id, k.order_count FROM orders AS o CROSS JOIN kept AS k", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "an aggregate with GROUP BY can return many rows", + sql: "WITH totals AS (SELECT customer_id, COUNT(*) AS order_count FROM orders GROUP BY customer_id) \ + SELECT o.order_id FROM orders AS o CROSS JOIN totals AS t", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `totals` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "a windowed count is not an aggregate", + sql: "WITH totals AS (SELECT COUNT(*) OVER () AS order_count FROM orders) \ + SELECT o.order_id FROM orders AS o CROSS JOIN totals AS t", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `totals` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "LIMIT 1", + sql: "WITH latest AS (SELECT order_id FROM orders ORDER BY order_id DESC LIMIT 1) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN latest AS l", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "LIMIT 2 can return two rows", + sql: "WITH latest AS (SELECT order_id FROM orders ORDER BY order_id DESC LIMIT 2) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN latest AS l", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `latest` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "one-row VALUES", + sql: "WITH settings AS (SELECT * FROM (VALUES (1, 'EUR')) AS v(version, currency)) \ + SELECT o.order_id FROM orders AS o CROSS JOIN settings AS s", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "two-row VALUES", + sql: "WITH settings AS (SELECT * FROM (VALUES (1), (2)) AS v(version)) \ + SELECT o.order_id FROM orders AS o CROSS JOIN settings AS s", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `settings` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "QUALIFY ROW_NUMBER() = 1 without a partition", + sql: "WITH newest AS (SELECT order_id FROM orders QUALIFY ROW_NUMBER() OVER (ORDER BY order_id DESC) = 1) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN newest AS n", + dialect: "snowflake", + one_row_relations: &[], + expected_messages: &[], + }, + CrossJoinTestCase { + description: "QUALIFY ROW_NUMBER() = 1 with a partition keeps one row per partition", + sql: "WITH newest AS (SELECT order_id FROM orders QUALIFY ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY order_id DESC) = 1) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN newest AS n", + dialect: "snowflake", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `newest` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "a join inside the carrying CTE stops the proof", + sql: "WITH totals AS (SELECT COUNT(*) AS n FROM orders), \ + joined AS (SELECT t.n FROM totals AS t INNER JOIN customers AS c ON c.n = t.n) \ + SELECT o.order_id FROM orders AS o CROSS JOIN joined AS j", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `joined` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "UNNEST in a carrying CTE stops the proof", + sql: "WITH totals AS (SELECT LIST(order_id) AS ids FROM orders), \ + expanded AS (SELECT UNNEST(ids) AS order_id FROM totals) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN expanded AS e", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `expanded` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a set operation stops the proof", + sql: "WITH both_totals AS (SELECT COUNT(*) AS n FROM orders UNION ALL SELECT COUNT(*) AS n FROM customers) \ + SELECT o.order_id FROM orders AS o CROSS JOIN both_totals AS b", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `both_totals` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "the finding names the CTE that contains the join", + sql: "WITH checks AS (SELECT o.order_id FROM orders AS o CROSS JOIN customers AS c) \ + SELECT order_id FROM checks", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `customers` in CTE `checks` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a later join still multiplies the rows before it", + sql: "SELECT o.order_id FROM __sqb_lint_7__ AS p CROSS JOIN orders AS o CROSS JOIN customers AS c", + dialect: "duckdb", + one_row_relations: &[POLICY_SENTINEL], + expected_messages: &[ + "CROSS JOIN with `customers` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "an unnamed relation says only a file-wide exception covers it", + sql: "SELECT o.order_id FROM orders AS o CROSS JOIN UNNEST([1, 2]) AS u(n)", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[UNNAMED_MESSAGE], + }, + CrossJoinTestCase { + description: "a named window is never treated as unpartitioned", + sql: "WITH newest AS (SELECT order_id FROM orders WINDOW w AS (PARTITION BY customer_id ORDER BY order_id DESC) QUALIFY ROW_NUMBER() OVER w = 1) \ + SELECT c.customer_id FROM customers AS c CROSS JOIN newest AS n", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &["CROSS JOIN with `newest` creates an explicit cartesian product"], + }, + CrossJoinTestCase { + description: "a join under a nested WITH names the top-level CTE", + sql: "WITH outer_checks AS (WITH inner_checks AS (SELECT o.order_id FROM orders AS o CROSS JOIN customers AS c) SELECT order_id FROM inner_checks) \ + SELECT order_id FROM outer_checks", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `customers` in CTE `outer_checks` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a WITH inside a subquery of the final select names no CTE", + sql: "SELECT s.order_id FROM (WITH inner_checks AS (SELECT o.order_id FROM orders AS o CROSS JOIN customers AS c) SELECT order_id FROM inner_checks) AS s", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `customers` creates an explicit cartesian product", + ], + }, + CrossJoinTestCase { + description: "a WITH owned by a set operation names its CTE", + sql: "WITH checks AS (SELECT o.order_id FROM orders AS o CROSS JOIN customers AS c) \ + SELECT order_id FROM checks UNION ALL SELECT order_id FROM orders", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "CROSS JOIN with `customers` in CTE `checks` creates an explicit cartesian product", + ], + }, + ]; + + for test_case in test_cases { + let messages: Vec = cross_join_messages(&test_case)?; + + assert_eq!( + messages, test_case.expected_messages, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_unnamed_cross_join_when_linting_then_help_prints_a_file_wide_exception() +-> Result<(), String> { + let test_cases = [CrossJoinTestCase { + description: "CROSS JOIN UNNEST", + sql: "SELECT o.order_id FROM orders AS o CROSS JOIN UNNEST([1, 2]) AS u(n)", + dialect: "duckdb", + one_row_relations: &[], + expected_messages: &[ + "[[rules.rule_exceptions]]\n rule = \"SQC038\"\n path = \"\"\n reason = \"\"", + ], + }]; + + for test_case in test_cases { + let remediations: Vec = cross_join_remediations(&test_case)?; + + assert_eq!(remediations.len(), 1, "{}", test_case.description); + assert!( + remediations[0].contains(test_case.expected_messages[0]), + "{}: {remediations:?}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_cross_join_messages_when_reading_their_subject_then_relation_and_cte_are_named() { + let test_cases = [ + CrossJoinSubjectTestCase { + description: "a top-level join", + message: "CROSS JOIN with `order_policy` creates an explicit cartesian product", + expected_subject: Some(("order_policy", None)), + }, + CrossJoinSubjectTestCase { + description: "a join inside a CTE", + message: "CROSS JOIN with `order_policy` in CTE `order_checks` creates an explicit cartesian product", + expected_subject: Some(("order_policy", Some("order_checks"))), + }, + CrossJoinSubjectTestCase { + description: "the generic message names nothing", + message: GENERIC_MESSAGE, + expected_subject: None, + }, + ]; + + for test_case in test_cases { + let subject = cross_join_subject(test_case.message); + + assert_eq!( + subject + .as_ref() + .map(|(relation, cte)| (relation.as_str(), cte.as_deref())), + test_case.expected_subject, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_empty_fixture_lint.rs b/crates/sqlclean/src/sql_lint/tests/test_empty_fixture_lint.rs new file mode 100644 index 0000000..db4988b --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_empty_fixture_lint.rs @@ -0,0 +1,61 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::engine::lint_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_empty_fixture_context_when_linting_projected_star_then_only_canonical_inputs_pass() +-> Result<(), String> { + let test_cases = [ + test_types::EmptyFixtureLintTestCase { + description: "canonical ref fixture", + sql: "WITH __ref__orders AS (SELECT * FROM __empty_fixture()) SELECT 1", + allows_empty_fixture_star: true, + expected_diagnostic_count: 0, + }, + test_types::EmptyFixtureLintTestCase { + description: "canonical source fixture", + sql: "WITH __source__web__orders AS (SELECT * FROM __empty_fixture()) SELECT 1", + allows_empty_fixture_star: true, + expected_diagnostic_count: 0, + }, + test_types::EmptyFixtureLintTestCase { + description: "ordinary SQL has no intrinsic exemption", + sql: "SELECT * FROM __empty_fixture()", + allows_empty_fixture_star: false, + expected_diagnostic_count: 1, + }, + test_types::EmptyFixtureLintTestCase { + description: "expected fixture remains controlled", + sql: "WITH __expected__orders AS (SELECT * FROM __empty_fixture()) SELECT 1", + allows_empty_fixture_star: true, + expected_diagnostic_count: 1, + }, + test_types::EmptyFixtureLintTestCase { + description: "additional projection is not canonical", + sql: "WITH __ref__orders AS (SELECT *, 1 AS extra FROM __empty_fixture()) SELECT 1", + allows_empty_fixture_star: true, + expected_diagnostic_count: 1, + }, + ]; + for test_case in test_cases { + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "snowflake", + "enabled_rules": ["SQC021"], + "allows_empty_fixture_star": test_case.allows_empty_fixture_star + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["diagnostics"].as_array().map_or(0, Vec::len), + test_case.expected_diagnostic_count, + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs b/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs new file mode 100644 index 0000000..c40d2ff --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_format_corpus.rs @@ -0,0 +1,61 @@ +use serde_json::Value; + +use crate::sql_lint::tests::helpers::check_dialect_corpus; +use crate::sql_lint::tests::test_types; + +const FORMAT_CORPUS: &str = include_str!("fixtures/format_corpus.json"); + +#[test] +fn given_dialect_corpus_when_formatting_then_output_layout_and_refusals_are_stable() +-> Result<(), String> { + let test_cases = [ + test_types::FormatCorpusTestCase { + description: "BigQuery identity corpus", + dialect: "bigquery", + expected_formatted: 168, + expected_refused: 0, + }, + test_types::FormatCorpusTestCase { + description: "Databricks identity corpus", + dialect: "databricks", + expected_formatted: 62, + expected_refused: 0, + }, + test_types::FormatCorpusTestCase { + description: "DuckDB identity corpus", + dialect: "duckdb", + expected_formatted: 178, + expected_refused: 11, + }, + test_types::FormatCorpusTestCase { + description: "PostgreSQL identity corpus", + dialect: "postgres", + expected_formatted: 132, + expected_refused: 5, + }, + test_types::FormatCorpusTestCase { + description: "Snowflake identity corpus", + dialect: "snowflake", + expected_formatted: 429, + expected_refused: 6, + }, + test_types::FormatCorpusTestCase { + description: "T-SQL identity corpus", + dialect: "tsql", + expected_formatted: 75, + expected_refused: 0, + }, + ]; + let corpus: Vec = + serde_json::from_str(FORMAT_CORPUS).map_err(|error| error.to_string())?; + for test_case in test_cases { + let (formatted, refused) = check_dialect_corpus(&corpus, test_case.dialect)?; + assert_eq!( + (formatted, refused), + (test_case.expected_formatted, test_case.expected_refused), + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_function_call_spacing.rs b/crates/sqlclean/src/sql_lint/tests/test_function_call_spacing.rs new file mode 100644 index 0000000..4acd30c --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_function_call_spacing.rs @@ -0,0 +1,117 @@ +use crate::sql_lint::tests::helpers::function_call_spacing; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_builtin_call_names_when_formatting_then_no_space_enters_the_call() -> Result<(), String> { + let test_cases = [ + test_types::FunctionCallSpacingTestCase { + description: "duckdb compact calls", + dialect: "duckdb", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "duckdb breaking calls", + dialect: "duckdb", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "snowflake compact calls", + dialect: "snowflake", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "snowflake breaking calls", + dialect: "snowflake", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "bigquery compact calls", + dialect: "bigquery", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "bigquery breaking calls", + dialect: "bigquery", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "postgres compact calls", + dialect: "postgres", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "postgres breaking calls", + dialect: "postgres", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "tsql compact calls", + dialect: "tsql", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "tsql breaking calls", + dialect: "tsql", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "databricks compact calls", + dialect: "databricks", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "databricks breaking calls", + dialect: "databricks", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "generic compact calls", + dialect: "generic", + line_width: 100, + argument: "a", + expected_refused: &[], + }, + test_types::FunctionCallSpacingTestCase { + description: "generic breaking calls", + dialect: "generic", + line_width: 40, + argument: "order_amount_with_a_long_descriptive_name", + expected_refused: &[], + }, + ]; + for test_case in &test_cases { + let (refused, misplaced) = + function_call_spacing(test_case.dialect, test_case.line_width, test_case.argument)?; + assert_eq!( + refused, test_case.expected_refused, + "{}", + test_case.description + ); + assert_eq!(misplaced, Vec::::new(), "{}", test_case.description); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_idempotency_reprint.rs b/crates/sqlclean/src/sql_lint/tests/test_idempotency_reprint.rs new file mode 100644 index 0000000..edf70b5 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_idempotency_reprint.rs @@ -0,0 +1,48 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::_helpers::formatter::RELAYOUTS; +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_formatted_and_unformatted_bodies_when_formatting_then_only_changed_bodies_are_reprinted() +-> Result<(), String> { + let test_cases = [ + test_types::ReprintTestCase { + description: "an already formatted body is not re-printed", + sql: "SELECT\n order_id,\n amount\nFROM orders\nWHERE amount > 0", + expected_changed: false, + expected_reprints: 0, + }, + test_types::ReprintTestCase { + description: "a body the formatter changes is re-printed once", + sql: "select order_id, amount from orders where amount > 0", + expected_changed: true, + expected_reprints: 1, + }, + ]; + for test_case in &test_cases { + RELAYOUTS.with(|relayouts| relayouts.set(0)); + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "duckdb"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["formatted"], true, + "{}: {payload}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}: {payload}", + test_case.description + ); + assert_eq!( + RELAYOUTS.with(std::cell::Cell::get), + test_case.expected_reprints, + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs b/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs new file mode 100644 index 0000000..55ff464 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs @@ -0,0 +1,287 @@ +use serde_json::Value; + +use crate::sql_lint::tests::helpers::format_at_width; +use crate::sql_lint::tests::test_types; +use crate::sql_tokens::main::query_fingerprint::query_fingerprint; + +#[test] +fn given_clause_layout_rules_when_formatting_then_each_rule_applies_idempotently() +-> Result<(), String> { + let test_cases = [ + test_types::LayoutRuleTestCase { + description: "a long Snowflake FROM VALUES puts each unsplit row on its own line", + dialect: "snowflake", + sql: "SELECT * FROM VALUES (1, '2024-01-15', 'PAID', 'north warehouse', 'priority shipping'), (2, '2024-01-16', 'OPEN', 'south warehouse', 'standard shipping')", + line_width: 60, + expected_sql: "SELECT *\nFROM VALUES\n (1, '2024-01-15', 'PAID', 'north warehouse', 'priority shipping'),\n (2, '2024-01-16', 'OPEN', 'south warehouse', 'standard shipping')", + }, + test_types::LayoutRuleTestCase { + description: "a short VALUES list stays on one line", + dialect: "snowflake", + sql: "SELECT * FROM VALUES ('a', 'A'), ('b', 'B')", + line_width: 100, + expected_sql: "SELECT *\nFROM VALUES ('a', 'A'), ('b', 'B')", + }, + test_types::LayoutRuleTestCase { + description: "a long parenthesized PostgreSQL VALUES moves under its parenthesis", + dialect: "postgres", + sql: "SELECT order_id, status FROM (VALUES (1, 'paid and shipped to customer'), (2, 'cancelled by customer')) AS orders(order_id, status)", + line_width: 60, + expected_sql: "SELECT\n order_id,\n status\nFROM (\n VALUES\n (1, 'paid and shipped to customer'),\n (2, 'cancelled by customer')\n) AS orders(order_id, status)", + }, + test_types::LayoutRuleTestCase { + description: "a long T-SQL VALUES relation keeps rows whole", + dialect: "tsql", + sql: "SELECT order_id FROM (VALUES (1, 'paid and shipped to customer'), (2, 'cancelled by customer')) AS orders(order_id, status)", + line_width: 60, + expected_sql: "SELECT order_id\nFROM (\n VALUES\n (1, 'paid and shipped to customer'),\n (2, 'cancelled by customer')\n) AS orders(order_id, status)", + }, + test_types::LayoutRuleTestCase { + description: "DuckDB INSERT VALUES rows stay whole when a row is over the width", + dialect: "duckdb", + sql: "INSERT INTO orders VALUES (1, 'paid and shipped to the customer by priority courier'), (2, 'open')", + line_width: 40, + expected_sql: "INSERT INTO orders\nVALUES\n (1, 'paid and shipped to the customer by priority courier'),\n (2, 'open')", + }, + test_types::LayoutRuleTestCase { + description: "Snowflake CAST breaks around IFF, not inside NUMBER(38, 2)", + dialect: "snowflake", + sql: "SELECT CAST(IFF(amount > 100000000, amount * 1000000000000, amount / 100000000000000) AS NUMBER(38, 2)) AS total FROM orders", + line_width: 80, + expected_sql: "SELECT\n CAST(\n IFF(\n amount > 100000000,\n amount * 1000000000000,\n amount / 100000000000000\n ) AS NUMBER(38, 2)\n ) AS total\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a DuckDB :: cast keeps DECIMAL(18, 4) whole", + dialect: "duckdb", + sql: "SELECT COALESCE(first_order_amount, second_order_amount)::DECIMAL(18, 4) AS amount FROM orders", + line_width: 50, + expected_sql: "SELECT\n COALESCE(\n first_order_amount,\n second_order_amount\n )::DECIMAL(18, 4) AS amount\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a BigQuery NUMERIC(38, 9) cast keeps its parameters", + dialect: "bigquery", + sql: "SELECT CAST(COALESCE(first_order_amount, second_order_amount) AS NUMERIC(38, 9)) AS amount FROM orders", + line_width: 50, + expected_sql: "SELECT\n CAST(\n COALESCE(\n first_order_amount,\n second_order_amount\n ) AS NUMERIC(38, 9)\n ) AS amount\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a T-SQL VARCHAR(MAX) cast keeps its parameter", + dialect: "tsql", + sql: "SELECT CAST(COALESCE(first_order_note, second_order_note) AS VARCHAR(MAX)) AS note FROM orders", + line_width: 50, + expected_sql: "SELECT\n CAST(\n COALESCE(\n first_order_note,\n second_order_note\n ) AS VARCHAR(MAX)\n ) AS note\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a Databricks DECIMAL(10, 2) cast keeps its parameters", + dialect: "databricks", + sql: "SELECT CAST(COALESCE(first_order_amount, second_order_amount) AS DECIMAL(10, 2)) AS amount FROM orders", + line_width: 50, + expected_sql: "SELECT\n CAST(\n COALESCE(\n first_order_amount,\n second_order_amount\n ) AS DECIMAL(10, 2)\n ) AS amount\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a long Snowflake DECODE puts each pair and the default on its own line", + dialect: "snowflake", + sql: "SELECT DECODE(status, 1, 'new', 2, 'open', 3, 'paid', 4, 'shipped', 5, 'cancelled', 'unknown') AS status_name FROM orders", + line_width: 60, + expected_sql: "SELECT\n DECODE(\n status,\n 1, 'new',\n 2, 'open',\n 3, 'paid',\n 4, 'shipped',\n 5, 'cancelled',\n 'unknown'\n ) AS status_name\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a DECODE without a default ends with a pair", + dialect: "snowflake", + sql: "SELECT DECODE(status, 1, 'new', 2, 'open', 3, 'paid', 4, 'shipped', 5, 'cancelled') AS status_name FROM orders", + line_width: 60, + expected_sql: "SELECT\n DECODE(\n status,\n 1, 'new',\n 2, 'open',\n 3, 'paid',\n 4, 'shipped',\n 5, 'cancelled'\n ) AS status_name\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a DECODE the layout guide breaks is regrouped into pairs", + dialect: "snowflake", + sql: "SELECT DECODE(status, 1, 'new', 2, 'open', 3, 'paid', 4, 'shipped', 5, 'cancelled', 6, 'returned', 'unknown') AS status_name FROM orders", + line_width: 100, + expected_sql: "SELECT\n DECODE(\n status,\n 1, 'new',\n 2, 'open',\n 3, 'paid',\n 4, 'shipped',\n 5, 'cancelled',\n 6, 'returned',\n 'unknown'\n ) AS status_name\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a short DECODE stays on one line", + dialect: "snowflake", + sql: "SELECT DECODE(status, 1, 'new', 'other') AS status_name FROM orders", + line_width: 100, + expected_sql: "SELECT DECODE(status, 1, 'new', 'other') AS status_name\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "an operator after a multi-line CASE starts the next line", + dialect: "duckdb", + sql: "SELECT CASE WHEN status = 'paid' THEN 'customer paid in full' WHEN status = 'open' THEN 'customer has not paid yet' ELSE 'other' END || CASE WHEN rush THEN '!' ELSE '' END AS label FROM orders", + line_width: 100, + expected_sql: "SELECT\n CASE\n WHEN status = 'paid'\n THEN 'customer paid in full'\n WHEN status = 'open'\n THEN 'customer has not paid yet'\n ELSE 'other'\n END\n || CASE WHEN rush THEN '!' ELSE '' END AS label\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "AND after a multi-line subquery starts the next line", + dialect: "postgres", + sql: "SELECT order_id FROM orders WHERE customer_id IN (SELECT customer_id FROM customers WHERE region = 'north') AND EXISTS (SELECT 1 FROM shipments)", + line_width: 100, + expected_sql: "SELECT order_id\nFROM orders\nWHERE\n customer_id IN (\n SELECT customer_id\n FROM customers\n WHERE region = 'north'\n )\n AND EXISTS(\n SELECT 1\n FROM shipments\n )", + }, + test_types::LayoutRuleTestCase { + description: "BETWEEN bounds keep their AND", + dialect: "snowflake", + sql: "SELECT order_id FROM orders WHERE amount BETWEEN 1 AND 9", + line_width: 100, + expected_sql: "SELECT order_id\nFROM orders\nWHERE amount BETWEEN 1 AND 9", + }, + test_types::LayoutRuleTestCase { + description: "CTEs and the final statement are separated by one blank line", + dialect: "duckdb", + sql: "with orders as (select * from raw_orders),\n\n\n-- paid only\npaid as (select * from orders where amount > 0)\n\n\nselect * from paid", + line_width: 100, + expected_sql: "WITH orders AS (\n SELECT *\n FROM raw_orders\n),\n\n-- paid only\npaid AS (\n SELECT *\n FROM orders\n WHERE amount > 0\n)\n\nSELECT *\nFROM paid", + }, + test_types::LayoutRuleTestCase { + description: "a trailing comment after a CTE comma keeps its line", + dialect: "snowflake", + sql: "WITH orders AS (SELECT * FROM raw_orders), -- raw\npaid AS (SELECT * FROM orders) SELECT * FROM paid", + line_width: 100, + expected_sql: "WITH orders AS (\n SELECT *\n FROM raw_orders\n), -- raw\n\npaid AS (\n SELECT *\n FROM orders\n)\n\nSELECT *\nFROM paid", + }, + test_types::LayoutRuleTestCase { + description: "a nested WITH is separated at its own indentation", + dialect: "postgres", + sql: "SELECT * FROM (WITH a AS MATERIALIZED (SELECT 1 AS x), b AS (SELECT x FROM a) SELECT x FROM b) AS nested", + line_width: 100, + expected_sql: "SELECT *\nFROM (\n WITH a AS MATERIALIZED (\n SELECT 1 AS x\n ),\n\n b AS (\n SELECT x\n FROM a\n )\n\n SELECT x\n FROM b\n) AS nested", + }, + test_types::LayoutRuleTestCase { + description: "BigQuery recursive CTEs with column lists are separated", + dialect: "bigquery", + sql: "WITH RECURSIVE numbers (n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM numbers WHERE n < 3), doubled AS (SELECT n * 2 AS n FROM numbers) SELECT n FROM doubled", + line_width: 100, + expected_sql: "WITH RECURSIVE numbers (n) AS (\n SELECT 1\n UNION ALL\n SELECT n + 1\n FROM numbers\n WHERE n < 3\n),\n\ndoubled AS (\n SELECT n * 2 AS n\n FROM numbers\n)\n\nSELECT n\nFROM doubled", + }, + test_types::LayoutRuleTestCase { + description: "a T-SQL WITH is separated before INSERT", + dialect: "tsql", + sql: "WITH paid AS (SELECT order_id FROM orders) INSERT INTO paid_orders SELECT order_id FROM paid", + line_width: 100, + expected_sql: "WITH paid AS (\n SELECT order_id\n FROM orders\n)\n\nINSERT INTO paid_orders\nSELECT order_id\nFROM paid", + }, + test_types::LayoutRuleTestCase { + description: "a CTE macro placeholder is separated like a CTE", + dialect: "duckdb", + sql: "WITH __sqb_lint_0__, paid AS (SELECT * FROM orders) SELECT * FROM paid", + line_width: 100, + expected_sql: "WITH\n__sqb_lint_0__,\n\npaid AS (\n SELECT *\n FROM orders\n)\n\nSELECT *\nFROM paid", + }, + test_types::LayoutRuleTestCase { + description: "comma-less CTE macro placeholders in a row are separated like CTEs", + dialect: "duckdb", + sql: "WITH a AS (SELECT 1 AS x), __sqb_lint_0__ __sqb_lint_1__ paid AS (SELECT * FROM a) SELECT * FROM paid", + line_width: 100, + expected_sql: "WITH a AS (\n SELECT 1 AS x\n),\n\n__sqb_lint_0__\n\n__sqb_lint_1__\npaid AS (\n SELECT *\n FROM a\n)\nSELECT *\nFROM paid", + }, + test_types::LayoutRuleTestCase { + description: "an alias and comma that push a call over the width break the call", + dialect: "snowflake", + sql: "SELECT ARRAY_CONTAINS('web'::VARIANT, COALESCE(o.order_channels, ARRAY_CONSTRUCT())) AS has_web_channel, o.order_id FROM orders o", + line_width: 80, + expected_sql: "SELECT\n ARRAY_CONTAINS(\n 'web'::VARIANT,\n COALESCE(o.order_channels, ARRAY_CONSTRUCT())\n ) AS has_web_channel,\n o.order_id\nFROM orders o", + }, + test_types::LayoutRuleTestCase { + description: "an AND chain inside a call argument joins its line, then wraps", + dialect: "snowflake", + sql: "SELECT CAST(MAX(IFF(line_number = 1 AND shipped_after_days = 960, order_amount, NULL)) AS NUMBER(38, 10)) AS first_line_amount_shipped_late FROM orders", + line_width: 100, + expected_sql: "SELECT\n CAST(\n MAX(IFF(line_number = 1 AND shipped_after_days = 960, order_amount, NULL)) AS NUMBER(38, 10)\n ) AS first_line_amount_shipped_late\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a one-line CASE over the width breaks at its branches", + dialect: "duckdb", + sql: "SELECT CASE WHEN o.shipped_at IS NULL THEN NULL WHEN o.refund_amount IS NULL THEN 0 ELSE 1 END AS has_refund FROM orders o", + line_width: 80, + expected_sql: "SELECT\n CASE\n WHEN o.shipped_at IS NULL\n THEN NULL\n WHEN o.refund_amount IS NULL\n THEN 0\n ELSE 1\n END AS has_refund\nFROM orders o", + }, + test_types::LayoutRuleTestCase { + description: "a WITHIN GROUP line breaks the aggregate call", + dialect: "snowflake", + sql: "SELECT customer_id, LISTAGG(DISTINCT LOWER(product_category_name), ', ') WITHIN GROUP (ORDER BY product_category_name) AS categories FROM orders GROUP BY customer_id", + line_width: 80, + expected_sql: "SELECT\n customer_id,\n LISTAGG(\n DISTINCT LOWER(product_category_name),\n ', '\n ) WITHIN GROUP (ORDER BY product_category_name) AS categories\nFROM orders\nGROUP BY\n customer_id", + }, + test_types::LayoutRuleTestCase { + description: "a window that continues on later lines breaks at its clauses", + dialect: "snowflake", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY CASE o.status WHEN 'paid' THEN 0 WHEN 'open' THEN 1 ELSE 2 END, o.ordered_at DESC) AS rn FROM orders o", + line_width: 60, + expected_sql: "SELECT\n ROW_NUMBER() OVER (\n PARTITION BY o.customer_id\n ORDER BY CASE o.status\n WHEN 'paid'\n THEN 0\n WHEN 'open'\n THEN 1\n ELSE 2\n END, o.ordered_at DESC\n ) AS rn\nFROM orders o", + }, + test_types::LayoutRuleTestCase { + description: "SELECT * and a single WHERE condition share their clause lines", + dialect: "duckdb", + sql: "select * from orders where amount > 0", + line_width: 100, + expected_sql: "SELECT *\nFROM orders\nWHERE amount > 0", + }, + test_types::LayoutRuleTestCase { + description: "SELECT DISTINCT keeps its single item, several items break", + dialect: "snowflake", + sql: "SELECT DISTINCT order_id FROM orders WHERE amount > 0 AND status = 'paid' UNION ALL SELECT order_id, customer_id FROM returns", + line_width: 100, + expected_sql: "SELECT DISTINCT order_id\nFROM orders\nWHERE\n amount > 0\n AND status = 'paid'\nUNION ALL\nSELECT\n order_id,\n customer_id\nFROM returns", + }, + test_types::LayoutRuleTestCase { + description: "T-SQL TOP, HAVING and Snowflake-style single items share lines", + dialect: "tsql", + sql: "SELECT TOP 10 COUNT(*) AS n FROM orders GROUP BY customer_id HAVING COUNT(*) > 1", + line_width: 100, + expected_sql: "SELECT TOP 10 COUNT(*) AS n\nFROM orders\nGROUP BY\n customer_id\nHAVING COUNT(*) > 1", + }, + test_types::LayoutRuleTestCase { + description: "QUALIFY with one condition shares its line", + dialect: "databricks", + sql: "SELECT order_id, ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at) AS rn FROM orders QUALIFY rn = 1", + line_width: 100, + expected_sql: "SELECT\n order_id,\n ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at) AS rn\nFROM orders\nQUALIFY rn = 1", + }, + test_types::LayoutRuleTestCase { + description: "a single item that does not fit stays under SELECT", + dialect: "bigquery", + sql: "SELECT COALESCE(first_order_amount, second_order_amount, third_order_amount) AS amount FROM orders WHERE first_order_amount + second_order_amount + third_order_amount > 100", + line_width: 60, + expected_sql: "SELECT\n COALESCE(\n first_order_amount,\n second_order_amount,\n third_order_amount\n ) AS amount\nFROM orders\nWHERE\n first_order_amount\n + second_order_amount\n + third_order_amount > 100", + }, + test_types::LayoutRuleTestCase { + description: "a short CASE item shares the SELECT line", + dialect: "duckdb", + sql: "SELECT CASE WHEN amount > 0 THEN 'paid' ELSE 'open' END AS status FROM orders", + line_width: 100, + expected_sql: "SELECT CASE WHEN amount > 0 THEN 'paid' ELSE 'open' END AS status\nFROM orders", + }, + test_types::LayoutRuleTestCase { + description: "a multi-line single item stays under SELECT", + dialect: "duckdb", + sql: "SELECT CASE WHEN amount > 1000 THEN 'large paid order' WHEN amount > 0 THEN 'paid order' ELSE 'open order' END AS status FROM orders", + line_width: 100, + expected_sql: "SELECT\n CASE\n WHEN amount > 1000\n THEN 'large paid order'\n WHEN amount > 0\n THEN 'paid order'\n ELSE 'open order'\n END AS status\nFROM orders", + }, + ]; + for test_case in &test_cases { + let response: Value = + format_at_width(test_case.sql, test_case.dialect, test_case.line_width)?; + assert_eq!(response["formatted"], true, "{}", test_case.description); + assert_eq!( + response["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + query_fingerprint(test_case.expected_sql, test_case.dialect)?, + query_fingerprint(test_case.sql, test_case.dialect)?, + "{}: layout changed the fingerprint", + test_case.description + ); + let again: Value = format_at_width( + test_case.expected_sql, + test_case.dialect, + test_case.line_width, + )?; + assert_eq!(again["changed"], false, "{}", test_case.description); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs b/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs new file mode 100644 index 0000000..3ae97ec --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs @@ -0,0 +1,107 @@ +use std::collections::HashMap; + +use serde_json::{Value, json}; + +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_lines_over_the_width_when_formatting_then_each_wrap_rule_applies_idempotently() +-> Result<(), String> { + let test_cases = [ + test_types::LineWrapTestCase { + description: "function arguments go one per line", + sql: "select coalesce(first_order_amount, second_order_amount, third_order_amount, fallback_amount) as amount from orders", + line_width: 60, + token_widths: &[], + expected_sql: "SELECT\n COALESCE(\n first_order_amount,\n second_order_amount,\n third_order_amount,\n fallback_amount\n ) AS amount\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "IN lists go one element per line", + sql: "select order_id from orders where region in ('north', 'south', 'east', 'west', 'central', 'remote')", + line_width: 50, + token_widths: &[], + expected_sql: "SELECT order_id\nFROM orders\nWHERE\n region IN (\n 'north',\n 'south',\n 'east',\n 'west',\n 'central',\n 'remote'\n )", + }, + test_types::LineWrapTestCase { + description: "window PARTITION BY and ORDER BY go on separate lines", + sql: "select row_number() over (partition by customer_id, region_code order by ordered_at desc, order_id desc) as rank_in_customer from orders", + line_width: 60, + token_widths: &[], + expected_sql: "SELECT\n ROW_NUMBER() OVER (\n PARTITION BY customer_id, region_code\n ORDER BY ordered_at DESC, order_id DESC\n ) AS rank_in_customer\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "join conditions break before AND but not inside BETWEEN", + sql: "select o.order_id from orders o join customers c on o.customer_id = c.customer_id and o.amount between 1 and 9 and o.store = c.store", + line_width: 60, + token_widths: &[], + expected_sql: "SELECT o.order_id\nFROM orders o\nJOIN customers c\n ON o.customer_id = c.customer_id\n AND o.amount BETWEEN 1 AND 9\n AND o.store = c.store", + }, + test_types::LineWrapTestCase { + description: "arithmetic chains break before operators", + sql: "select first_amount + second_amount + third_amount + fourth_amount as total from orders", + line_width: 50, + token_widths: &[], + expected_sql: "SELECT\n first_amount\n + second_amount\n + third_amount\n + fourth_amount AS total\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "a top-level AND chain breaks before a nested group", + sql: "select order_id from orders where cast(ordered_at as DATE) >= cast(@start as DATE) and cast(ordered_at as DATE) < cast(@end as DATE)", + line_width: 60, + token_widths: &[], + expected_sql: "SELECT order_id\nFROM orders\nWHERE\n CAST(ordered_at AS DATE) >= CAST(@start AS DATE)\n AND CAST(ordered_at AS DATE) < CAST(@end AS DATE)", + }, + test_types::LineWrapTestCase { + description: "a long string literal is never broken", + sql: "select 'a long literal that is wider than the configured line width' as note from orders", + line_width: 40, + token_widths: &[], + expected_sql: "SELECT\n 'a long literal that is wider than the configured line width' AS note\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "a line with a comment is never broken", + sql: "select coalesce(first_amount, second_amount, third_amount) as amount -- keep\nfrom orders", + line_width: 40, + token_widths: &[], + expected_sql: "SELECT\n COALESCE(first_amount, second_amount, third_amount) AS amount -- keep\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "a sentinel counts with the width of the text it stands for", + sql: "select coalesce(__sqb_lint_0__, b) as amount from orders", + line_width: 40, + token_widths: &[("__SQB_LINT_0__", 30)], + expected_sql: "SELECT\n COALESCE(\n __sqb_lint_0__,\n b\n ) AS amount\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "lines within the width are unchanged", + sql: "select coalesce(first_amount, second_amount) as amount from orders", + line_width: 100, + token_widths: &[], + expected_sql: "SELECT COALESCE(first_amount, second_amount) AS amount\nFROM orders", + }, + ]; + for test_case in &test_cases { + let token_widths: HashMap<&str, usize> = test_case.token_widths.iter().copied().collect(); + let request = |sql: &str| { + json!({ + "version": 1, + "sql": sql, + "dialect": "duckdb", + "line_width": test_case.line_width, + "token_widths": token_widths, + }) + .to_string() + }; + let response: Value = serde_json::from_str(&format_json(&request(test_case.sql))?) + .map_err(|error| error.to_string())?; + assert_eq!( + response["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + let again: Value = serde_json::from_str(&format_json(&request(test_case.expected_sql))?) + .map_err(|error| error.to_string())?; + assert_eq!(again["changed"], false, "{}", test_case.description); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_lint_exemptions.rs b/crates/sqlclean/src/sql_lint/tests/test_lint_exemptions.rs new file mode 100644 index 0000000..5ed8b24 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_lint_exemptions.rs @@ -0,0 +1,130 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::engine::lint_json; +use crate::sql_lint::tests::test_types::{ + DynamicOutputStarLintTestCase, SqlMacroOutputLintTestCase, +}; + +#[test] +fn given_sql_macro_output_when_linting_qualification_then_each_column_counts_as_written() +-> Result<(), String> { + let test_cases = [ + SqlMacroOutputLintTestCase { + description: "a bare column from an expression macro beside a qualified column fails", + sql: "SELECT o.order_id, status AS s FROM orders AS o", + output: "status", + relations: &[], + expected_count: 1, + }, + SqlMacroOutputLintTestCase { + description: "reads through a passed relation pass beside a bare caller column", + sql: "SELECT order_id, o.amount - o.discount AS net FROM orders AS o", + output: "o.amount - o.discount", + relations: &["o"], + expected_count: 0, + }, + SqlMacroOutputLintTestCase { + description: "reads through a passed relation pass beside a hand-qualified column", + sql: "SELECT o.order_id, o.amount - o.discount AS net FROM orders AS o", + output: "o.amount - o.discount", + relations: &["o"], + expected_count: 0, + }, + SqlMacroOutputLintTestCase { + description: "the caller's own mix beside reads through a passed relation fails", + sql: "SELECT o.order_id, status, o.amount AS a FROM orders AS o", + output: "o.amount", + relations: &["o"], + expected_count: 1, + }, + SqlMacroOutputLintTestCase { + description: "an aliased read counts as qualified beside a bare caller column", + sql: "SELECT order_id, s.amount AS a FROM orders AS s", + output: "s.amount", + relations: &["orders"], + expected_count: 1, + }, + SqlMacroOutputLintTestCase { + description: "a query scope the macro generated whole is judged in the macro file", + sql: "WITH c AS (SELECT a, w.b FROM w) SELECT * FROM c", + output: "SELECT a, w.b FROM w", + relations: &[], + expected_count: 0, + }, + ]; + for test_case in test_cases { + let start: usize = test_case + .sql + .find(test_case.output) + .ok_or("the output should be in the SQL")?; + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "duckdb", + "enabled_rules": ["SQC028"], + "sql_macro_outputs": [{ + "start": start, + "end": start + test_case.output.len(), + "relations": test_case.relations, + }], + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + let diagnostics = payload["diagnostics"] + .as_array() + .ok_or("diagnostics should be an array")?; + + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_compiler_proven_dynamic_output_when_linting_then_only_terminal_star_is_exempt() +-> Result<(), String> { + let test_cases = [DynamicOutputStarLintTestCase { + description: "unrelated CTE star remains controlled", + sql: "WITH pivot_input AS (SELECT customer_id, category, amount FROM orders), \ +final AS (SELECT * FROM pivot_input PIVOT(MAX(amount) FOR category IN (ANY))), \ +unrelated AS (SELECT * FROM pivot_input) SELECT * FROM final", + expected_count: 1, + expected_prefix: "unrelated AS (SELECT ", + }]; + for test_case in test_cases { + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "snowflake", + "enabled_rules": ["SQC021"], + "allows_dynamic_output_star": true, + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + let diagnostics = payload["diagnostics"] + .as_array() + .ok_or("diagnostics should be an array")?; + + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + let start = diagnostics[0]["start"].as_u64().unwrap_or_default() as usize; + assert!( + test_case.sql[..start].ends_with(test_case.expected_prefix), + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_set_operation_formatter.rs b/crates/sqlclean/src/sql_lint/tests/test_set_operation_formatter.rs new file mode 100644 index 0000000..9f8e7fb --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_set_operation_formatter.rs @@ -0,0 +1,59 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +const DIALECTS: [&str; 6] = [ + "bigquery", + "databricks", + "duckdb", + "postgres", + "snowflake", + "tsql", +]; + +#[test] +fn given_set_operation_after_scalar_subquery_operand_when_formatting_then_keyword_stays_outer() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "UNION ALL after WHERE scalar subquery", + sql: "SELECT o.id, o.total FROM orders AS o WHERE o.total < (SELECT MAX(l.amount) FROM limits AS l) UNION ALL SELECT o.id, o.total FROM orders AS o", + expected_sql: "SELECT\n o.id,\n o.total\nFROM orders AS o\nWHERE\n o.total < (\n SELECT MAX(l.amount)\n FROM limits AS l\n )\nUNION ALL\nSELECT\n o.id,\n o.total\nFROM orders AS o", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "EXCEPT after WHERE scalar subquery", + sql: "SELECT o.id FROM orders AS o WHERE o.total > (SELECT MAX(l.amount) FROM limits AS l) EXCEPT SELECT o.id FROM orders AS o", + expected_sql: "SELECT o.id\nFROM orders AS o\nWHERE\n o.total > (\n SELECT MAX(l.amount)\n FROM limits AS l\n )\nEXCEPT\nSELECT o.id\nFROM orders AS o", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "INTERSECT after WHERE scalar subquery", + sql: "SELECT o.id FROM orders AS o WHERE o.total > (SELECT MAX(l.amount) FROM limits AS l) INTERSECT SELECT o.id FROM orders AS o", + expected_sql: "SELECT o.id\nFROM orders AS o\nWHERE\n o.total > (\n SELECT MAX(l.amount)\n FROM limits AS l\n )\nINTERSECT\nSELECT o.id\nFROM orders AS o", + expected_changed: true, + }, + ]; + + for dialect in DIALECTS { + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": dialect}).to_string(), + )?; + let payload: Value = + serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{dialect}: {}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{dialect}: {}", + test_case.description + ); + } + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_sql_lint.rs b/crates/sqlclean/src/sql_lint/tests/test_sql_lint.rs new file mode 100644 index 0000000..4171cd0 --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_sql_lint.rs @@ -0,0 +1,1969 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::engine::lint_json; +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::helpers::format_batch_json; +use crate::sql_lint::tests::{helpers, test_types}; + +#[test] +fn given_production_function_depth_when_linting_then_bounded_parser_accepts_it() { + let test_cases = [test_types::FunctionDepthTestCase { + description: "production depth remains supported", + depth: 65, + expected_diagnostic_count: 0, + }]; + for test_case in test_cases { + let sql = helpers::nested_function_sql(test_case.depth); + let diagnostics = helpers::diagnostics(&sql).unwrap_or_else(|error| { + panic!("{}: {error}", test_case.description); + }); + assert_eq!( + diagnostics.len(), + test_case.expected_diagnostic_count, + "{}", + test_case.description + ); + } +} + +#[test] +fn given_excessive_function_depth_when_linting_then_complexity_guard_rejects_it() { + let test_cases = [test_types::FunctionDepthFailureTestCase { + description: "excessive depth remains bounded", + depth: 129, + expected_code: "E_GUARD_FUNCTION_NESTING_DEPTH_EXCEEDED", + expected_limit: "configured limit 128", + }]; + for test_case in test_cases { + let sql = helpers::nested_function_sql(test_case.depth); + let error = + helpers::diagnostics(&sql).expect_err("excessive nested calls should remain bounded"); + assert!( + error.contains(test_case.expected_code), + "{}", + test_case.description + ); + assert!( + error.contains(test_case.expected_limit), + "{}", + test_case.description + ); + } +} + +#[test] +fn given_sql_cases_when_linting_then_diagnostics_match() -> Result<(), String> { + let test_cases = [ + test_types::LintTestCase { + description: "NULL comparison", + sql: "SELECT value FROM a WHERE value = NULL", + expected_codes: &["SQC001"], + expected_anchors: &[("SQC001", "=")], + }, + test_types::LintTestCase { + description: "wrapped NULL comparison", + sql: "SELECT value FROM a WHERE (NULL) = value", + expected_codes: &["SQC001"], + expected_anchors: &[("SQC001", "=")], + }, + test_types::LintTestCase { + description: "implicit cartesian join", + sql: "SELECT a.id FROM a, b", + expected_codes: &["SQC002"], + expected_anchors: &[("SQC002", ",")], + }, + test_types::LintTestCase { + description: "unconditioned join", + sql: "SELECT a.id FROM a JOIN b", + expected_codes: &["SQC003"], + expected_anchors: &[("SQC003", "JOIN")], + }, + test_types::LintTestCase { + description: "unordered limit", + sql: "SELECT id FROM items LIMIT 1", + expected_codes: &["SQC004"], + expected_anchors: &[("SQC004", "LIMIT")], + }, + test_types::LintTestCase { + description: "limit zero selects no rows in any order", + sql: "SELECT id FROM items LIMIT 0", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "limit zero with an offset still selects no rows", + sql: "SELECT id FROM items LIMIT 0 OFFSET 5", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "semi-structured limit and offset keys are not row selection clauses", + sql: "SELECT payload:Limit::VARCHAR AS order_limit, payload:Offset::NUMBER AS order_offset FROM orders", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "semi-structured limit key does not hide a later row limit clause", + sql: "SELECT payload:Limit::VARCHAR AS order_limit FROM orders LIMIT 1", + expected_codes: &["SQC004"], + expected_anchors: &[("SQC004", "LIMIT")], + }, + test_types::LintTestCase { + description: "semi-structured limit key in a filter is not a row limit clause", + sql: "SELECT order_id FROM orders WHERE payload:Limit::NUMBER > 3", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "unused CTE", + sql: "WITH unused AS (SELECT 1) SELECT 1", + expected_codes: &["SQC005"], + expected_anchors: &[("SQC005", "unused")], + }, + test_types::LintTestCase { + description: "unreachable CTE chain", + sql: "WITH first AS (SELECT 1), second AS (SELECT * FROM first) SELECT 1", + expected_codes: &["SQC005", "SQC005"], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "unused CTE on set operation", + sql: "WITH unused AS (SELECT 1 AS id) SELECT id FROM left_table UNION ALL SELECT id FROM right_table", + expected_codes: &["SQC005"], + expected_anchors: &[("SQC005", "unused")], + }, + test_types::LintTestCase { + description: "redundant distinct", + sql: "SELECT DISTINCT id FROM items GROUP BY id", + expected_codes: &["SQC006"], + expected_anchors: &[("SQC006", "DISTINCT")], + }, + test_types::LintTestCase { + description: "aggregate distinct is not a select modifier", + sql: "SELECT category, COUNT(DISTINCT id) FROM items GROUP BY category", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "positional set star", + sql: "SELECT * FROM a UNION ALL SELECT * FROM b", + expected_codes: &["SQC007"], + expected_anchors: &[("SQC007", "UNION")], + }, + test_types::LintTestCase { + description: "parenthesized positional set star", + sql: "(SELECT * FROM a) UNION ALL (SELECT id FROM b)", + expected_codes: &["SQC007"], + expected_anchors: &[("SQC007", "UNION")], + }, + test_types::LintTestCase { + description: "nested set wildcard does not contaminate outer set operation", + sql: "SELECT 'actual' WHERE EXISTS (SELECT id FROM actual EXCEPT SELECT * FROM expected) UNION ALL SELECT 'expected'", + expected_codes: &["SQC007"], + expected_anchors: &[("SQC007", "EXCEPT")], + }, + test_types::LintTestCase { + description: "multiple statements", + sql: "SELECT value FROM first LIMIT 1; SELECT value FROM second OFFSET 1", + expected_codes: &["SQC004", "SQC004"], + expected_anchors: &[("SQC004", "LIMIT"), ("SQC004", "OFFSET")], + }, + test_types::LintTestCase { + description: "nested query anchors", + sql: "SELECT * FROM (SELECT id FROM inner_items ORDER BY id LIMIT 1) safe JOIN outer_items ON TRUE OFFSET 2", + expected_codes: &["SQC039", "SQC003", "SQC004"], + expected_anchors: &[ + ("SQC039", "SELECT"), + ("SQC003", "JOIN"), + ("SQC004", "OFFSET"), + ], + }, + test_types::LintTestCase { + description: "safe NULL predicate", + sql: "SELECT value FROM a WHERE value IS NULL", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "NULL inside function", + sql: "SELECT value FROM a WHERE COALESCE(value, NULL) = 1", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "NULL assignment", + sql: "UPDATE items SET value = NULL", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "cross join", + sql: "SELECT a.id FROM a CROSS JOIN b", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "keyed join", + sql: "SELECT a.id FROM a JOIN b ON a.id = b.id", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "ordered limit", + sql: "SELECT id FROM items ORDER BY id LIMIT 1", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "used CTE", + sql: "WITH used AS (SELECT 1 AS id) SELECT id FROM used", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "group without distinct", + sql: "SELECT id FROM items GROUP BY id", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "nonredundant distinct", + sql: "SELECT DISTINCT a FROM items GROUP BY a, b", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "within group", + sql: "SELECT DISTINCT LISTAGG(x, ',') WITHIN GROUP (ORDER BY x) FROM items", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "explicit positional set", + sql: "SELECT id FROM a UNION ALL SELECT id FROM b", + expected_codes: &[], + expected_anchors: &[], + }, + test_types::LintTestCase { + description: "nested star outside set arms", + sql: "SELECT id, name FROM (SELECT * FROM base) b UNION ALL SELECT id, name FROM other", + expected_codes: &["SQC039"], + expected_anchors: &[("SQC039", "SELECT")], + }, + test_types::LintTestCase { + description: "nested star inside wrapped set arm", + sql: "(SELECT id FROM (SELECT * FROM base) b) UNION ALL (SELECT id FROM other)", + expected_codes: &["SQC039"], + expected_anchors: &[("SQC039", "SELECT")], + }, + ]; + + for test_case in &test_cases { + let diagnostics = helpers::diagnostics(test_case.sql)?; + let codes: Vec<&str> = diagnostics + .iter() + .filter_map(|item| item["code"].as_str()) + .collect(); + let anchors: Vec<(&str, &str)> = diagnostics + .iter() + .filter_map(|item| { + let code = item["code"].as_str()?; + let start = item["start"].as_u64()? as usize; + let end = item["end"].as_u64()? as usize; + Some((code, &test_case.sql[start..end])) + }) + .collect(); + assert_eq!(codes, test_case.expected_codes, "{}", test_case.description); + for expected_anchor in test_case.expected_anchors { + assert!( + anchors.contains(expected_anchor), + "{}", + test_case.description + ); + } + } + Ok(()) +} + +#[test] +fn given_plain_sql_rules_when_linting_then_project_context_is_not_required() -> Result<(), String> { + let test_cases = [ + test_types::PlainSqlLintTestCase { + description: "trailing comment", + sql: "SELECT id -- why\nFROM items", + rule: "SQC033", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "detached comment", + sql: "SELECT id\n-- why\n\nFROM items", + rule: "SQC033", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "attached predicate comment", + sql: "SELECT id FROM items WHERE active\n-- retain settled rows\nAND settled", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached CTE comment", + sql: "WITH\n-- normalized rows\nnormalized AS (SELECT id FROM items)\nSELECT id FROM normalized", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached join comment", + sql: "SELECT left_rows.id FROM left_rows\n-- match stable identities\nJOIN right_rows ON left_rows.id = right_rows.id", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached filter comment", + sql: "SELECT id FROM items\n-- retain active rows\nWHERE active", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached projection comment", + sql: "SELECT\n-- stable identity\nid\nFROM items", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached CASE branch comment", + sql: "SELECT CASE\n-- settled outcome\nWHEN settled THEN 1 ELSE 0 END FROM items", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "attached ordering comment with Unicode and CRLF", + sql: "SELECT id FROM items\r\n-- deterministic café order\r\nORDER BY id", + rule: "SQC033", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "block comment", + sql: "SELECT id\n/* selected identity */\nFROM items", + rule: "SQC033", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "body without CTE", + sql: "SELECT id FROM items", + rule: "SQC034", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "logic in terminal select", + sql: "WITH final_rows AS (SELECT id FROM items) SELECT id FROM final_rows WHERE id > 0", + rule: "SQC034", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "terminal select reads wrong CTE", + sql: "WITH first_rows AS (SELECT 1 AS id), final_rows AS (SELECT id FROM first_rows) SELECT id FROM first_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "plain terminal select", + sql: "WITH first_rows AS (SELECT 1 AS id), final_rows AS (SELECT id FROM first_rows) SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "plain terminal select with keyword-named columns", + sql: "WITH final_rows AS (SELECT 1 AS date, 2 AS comment, 3 AS key, 4 AS percent, 5 AS sequence, 6 AS time) SELECT date, comment, key, percent, sequence, time FROM final_rows", + rule: "SQC035", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "terminal date literal remains an expression", + sql: "WITH final_rows AS (SELECT 1 AS id) SELECT DATE '2026-01-01' FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "terminal modulo calculation remains an expression", + sql: "WITH final_rows AS (SELECT 10 AS quantity, 3 AS pack_size) SELECT quantity % pack_size AS remainder FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "plain terminal select from CTE with output column list", + sql: "WITH final_rows(id) AS (SELECT 1) SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "CTE output column list names its calculation", + sql: "WITH final_rows(id) AS (SELECT 1) SELECT id FROM final_rows", + rule: "SQC022", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "terminal union remains outside final CTE", + sql: "WITH final_rows AS (SELECT 1 AS id) SELECT id FROM final_rows UNION ALL SELECT id FROM final_rows", + rule: "SQC034", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "terminal union is not a plain final CTE read", + sql: "WITH final_rows AS (SELECT 1 AS id) SELECT id FROM final_rows UNION ALL SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "parenthesized terminal union remains outside final CTE", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) UNION ALL (SELECT id FROM final_rows)", + rule: "SQC034", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "parenthesized terminal union is not a plain final CTE read", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) UNION ALL (SELECT id FROM final_rows)", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "mixed parenthesized terminal union remains outside final CTE", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) UNION ALL SELECT id FROM final_rows", + rule: "SQC034", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "mixed parenthesized terminal union is not a plain final CTE read", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) UNION ALL SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "mixed parenthesized terminal intersect is not a plain final CTE read", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) INTERSECT SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "mixed parenthesized terminal except is not a plain final CTE read", + sql: "WITH final_rows AS (SELECT 1 AS id) (SELECT id FROM final_rows) EXCEPT SELECT id FROM final_rows", + rule: "SQC035", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "nested CTE", + sql: "SELECT id FROM (WITH nested_rows AS (SELECT 1 AS id) SELECT id FROM nested_rows)", + rule: "SQC036", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "Snowflake hierarchy START WITH is not a nested CTE", + sql: "WITH entities AS (SELECT id, parent_id FROM source_rows), hierarchy AS (SELECT id FROM entities START WITH parent_id IS NULL CONNECT BY parent_id = PRIOR id) SELECT id FROM hierarchy", + rule: "SQC036", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "recursive CTE", + sql: "WITH RECURSIVE numbers AS (SELECT 1 UNION ALL SELECT 2) SELECT * FROM numbers", + rule: "SQC037", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "cross join", + sql: "SELECT left_rows.id FROM left_rows CROSS JOIN right_rows", + rule: "SQC038", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "inline query after from", + sql: "SELECT derived.order_id FROM (SELECT order_id FROM orders) AS derived", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "inline query after join", + sql: "SELECT orders.order_id FROM orders INNER JOIN (SELECT order_id FROM shipments) AS shipped_orders ON orders.order_id = shipped_orders.order_id", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "inline values query after full outer join", + sql: "SELECT COALESCE(actual.order_id, expected.order_id) AS order_id FROM actual FULL OUTER JOIN (SELECT column1 AS order_id FROM VALUES (1), (2)) AS expected ON actual.order_id = expected.order_id", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "inline values relation after join", + sql: "SELECT orders.order_id FROM orders INNER JOIN (VALUES (1), (2)) AS expected(order_id) ON orders.order_id = expected.order_id", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "inline nested with query", + sql: "SELECT derived.order_id FROM (WITH imported_orders AS (SELECT order_id FROM orders) SELECT order_id FROM imported_orders) AS derived", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "redundantly parenthesized inline query", + sql: "SELECT derived.order_id FROM (((SELECT order_id FROM orders))) AS derived", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "comma-separated inline query", + sql: "SELECT orders.order_id FROM orders, (SELECT order_id FROM shipments) AS shipped_orders", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery", + sql: "SELECT (SELECT MAX(order_id) FROM orders) AS latest_order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery containing a derived relation", + sql: "SELECT (SELECT derived.order_id FROM (SELECT nested.order_id FROM (SELECT 1 AS order_id) AS nested) AS derived) AS order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "exists subquery containing a derived relation", + sql: "SELECT order_id FROM orders WHERE EXISTS (SELECT 1 FROM (SELECT nested.order_id FROM (SELECT order_id FROM shipments) AS nested) AS shipped_orders WHERE shipped_orders.order_id = orders.order_id)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "exists set branch containing a derived relation", + sql: "SELECT EXISTS (SELECT 1 UNION ALL SELECT derived.order_id FROM (SELECT 1 AS order_id) AS derived)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar with query containing a derived relation in a CTE", + sql: "SELECT (WITH derived AS (SELECT nested.order_id FROM (SELECT 1 AS order_id) AS nested) SELECT order_id FROM derived) AS order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar with query containing a derived terminal relation", + sql: "SELECT (WITH orders AS (SELECT 1 AS order_id) SELECT derived.order_id FROM (SELECT order_id FROM orders) AS derived) AS order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "nested derived relations outside an excluded subquery", + sql: "SELECT derived.order_id FROM (SELECT nested.order_id FROM (SELECT 1 AS order_id) AS nested) AS derived", + rule: "SQC039", + expected_count: 2, + }, + test_types::PlainSqlLintTestCase { + description: "nested derived relations inside a parenthesized ordinary relation", + sql: "SELECT derived.order_id FROM ((SELECT nested.order_id FROM (SELECT 1 AS order_id) AS nested)) AS derived", + rule: "SQC039", + expected_count: 2, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery after projection comma", + sql: "SELECT 1 AS constant_value, (SELECT MAX(order_id) FROM orders) AS latest_order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery after expression from", + sql: "SELECT EXTRACT(YEAR FROM (SELECT MAX(order_date) FROM orders)) AS latest_order_year", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery after sibling projection subquery", + sql: "SELECT (SELECT MAX(order_id) FROM orders), COALESCE(NULL, (SELECT MAX(order_id) FROM orders))", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "expression from after sibling projection subquery", + sql: "SELECT (SELECT MAX(order_id) FROM orders), EXTRACT(YEAR FROM (SELECT MAX(order_date) FROM orders))", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery after order by comma", + sql: "SELECT order_id FROM orders ORDER BY order_id, (SELECT MAX(status_id) FROM statuses)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "scalar subquery with a derived relation after order by comma", + sql: "SELECT order_id FROM orders ORDER BY order_id, (SELECT derived.status_id FROM (SELECT 1 AS status_id) AS derived)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "exists subquery", + sql: "SELECT order_id FROM orders WHERE EXISTS (SELECT 1 FROM shipments WHERE shipments.order_id = orders.order_id)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "in subquery", + sql: "SELECT order_id FROM orders WHERE order_id IN (SELECT order_id FROM shipments)", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "named CTE relation", + sql: "WITH shipped_orders AS (SELECT order_id FROM shipments) SELECT orders.order_id FROM orders INNER JOIN shipped_orders ON orders.order_id = shipped_orders.order_id", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "materialized CTE containing a derived relation", + sql: "WITH orders AS MATERIALIZED (SELECT derived.order_id FROM (SELECT 1 AS order_id) AS derived) SELECT order_id FROM orders", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "not materialized CTE containing a derived relation", + sql: "WITH orders AS NOT MATERIALIZED (SELECT derived.order_id FROM (SELECT 1 AS order_id) AS derived) SELECT order_id FROM orders", + rule: "SQC039", + expected_count: 1, + }, + test_types::PlainSqlLintTestCase { + description: "lateral query relation", + sql: "SELECT orders.order_id FROM orders, LATERAL (SELECT orders.order_id) AS derived", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "apply query relation", + sql: "SELECT orders.order_id FROM orders CROSS APPLY (SELECT orders.order_id) AS derived", + rule: "SQC039", + expected_count: 0, + }, + test_types::PlainSqlLintTestCase { + description: "table function relation", + sql: "SELECT flattened.value FROM TABLE(FLATTEN(input => PARSE_JSON('[1]'))) AS flattened", + rule: "SQC039", + expected_count: 0, + }, + ]; + + for test_case in test_cases { + let diagnostics = helpers::diagnostics_for_rules(test_case.sql, &[test_case.rule])?; + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + assert!( + diagnostics + .iter() + .all(|diagnostic| diagnostic["code"] == test_case.rule), + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_native_rules_when_linting_then_diagnosis_and_remediation_are_actionable() +-> Result<(), String> { + let test_cases = [ + test_types::LintMetadataTestCase { + description: "NULL comparison guidance", + sql: "SELECT value FROM a WHERE value = NULL", + expected_code: "SQC001", + expected_message: "Comparison with NULL is never true", + expected_remediation: "Use IS NULL or IS NOT NULL when testing for NULL.", + }, + test_types::LintMetadataTestCase { + description: "implicit join guidance", + sql: "SELECT a.id FROM a, b", + expected_code: "SQC002", + expected_message: "Comma-separated sources create an implicit cartesian join", + expected_remediation: "Replace comma-separated sources with an explicit keyed join, or use CROSS JOIN when the cartesian product is intentional.", + }, + test_types::LintMetadataTestCase { + description: "unconditioned join guidance", + sql: "SELECT a.id FROM a JOIN b", + expected_code: "SQC003", + expected_message: "Non-cross join has no meaningful condition", + expected_remediation: "Add a meaningful ON or USING condition, or declare an intentional cartesian product with CROSS JOIN.", + }, + test_types::LintMetadataTestCase { + description: "unordered limit guidance", + sql: "SELECT id FROM items LIMIT 1", + expected_code: "SQC004", + expected_message: "Row selection is nondeterministic", + expected_remediation: "Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET.", + }, + test_types::LintMetadataTestCase { + description: "unused CTE guidance", + sql: "WITH unused AS (SELECT 1) SELECT 1", + expected_code: "SQC005", + expected_message: "CTE is unreachable from the final query", + expected_remediation: "Reference the CTE from the final query or another reachable CTE, or remove it.", + }, + test_types::LintMetadataTestCase { + description: "redundant distinct guidance", + sql: "SELECT DISTINCT id FROM items GROUP BY id", + expected_code: "SQC006", + expected_message: "DISTINCT is redundant with the grouped output", + expected_remediation: "Remove DISTINCT; the equivalent GROUP BY already determines the output groups.", + }, + test_types::LintMetadataTestCase { + description: "positional set star guidance", + sql: "SELECT * FROM a UNION ALL SELECT * FROM b", + expected_code: "SQC007", + expected_message: "Positional set operation is vulnerable to column-order drift", + expected_remediation: "Enumerate columns in the same order in every set-operation branch.", + }, + ]; + + for test_case in test_cases { + let diagnostics = helpers::diagnostics(test_case.sql)?; + let diagnostic = diagnostics + .first() + .ok_or_else(|| format!("{} should report a diagnostic", test_case.description))?; + assert_eq!( + diagnostic["code"], test_case.expected_code, + "{}", + test_case.description + ); + assert_eq!( + diagnostic["message"], test_case.expected_message, + "{}", + test_case.description + ); + assert_eq!( + diagnostic["remediation"], test_case.expected_remediation, + "{}", + test_case.description + ); + let start = diagnostic["start"].as_u64().unwrap_or_default(); + let end = diagnostic["end"].as_u64().unwrap_or_default(); + assert!( + end > start, + "{} should report a non-empty range", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_fixable_and_ambiguous_findings_when_linting_then_only_proven_edits_are_returned() +-> Result<(), String> { + let test_cases = [ + test_types::LintFixTestCase { + description: "right-hand NULL equality", + sql: "SELECT value FROM a WHERE value = NULL", + expected_code: "SQC001", + expected_replacement: Some("IS"), + }, + test_types::LintFixTestCase { + description: "right-hand NULL inequality", + sql: "SELECT value FROM a WHERE value <> NULL", + expected_code: "SQC001", + expected_replacement: Some("IS NOT"), + }, + test_types::LintFixTestCase { + description: "left-hand NULL comparison requires authored intent", + sql: "SELECT value FROM a WHERE NULL = value", + expected_code: "SQC001", + expected_replacement: None, + }, + test_types::LintFixTestCase { + description: "plain conditionless join", + sql: "SELECT a.id FROM a JOIN b", + expected_code: "SQC003", + expected_replacement: Some("CROSS JOIN"), + }, + test_types::LintFixTestCase { + description: "qualified conditionless join requires intent", + sql: "SELECT a.id FROM a LEFT JOIN b", + expected_code: "SQC003", + expected_replacement: None, + }, + test_types::LintFixTestCase { + description: "conditionless semi join is never rewritten as cross join", + sql: "SELECT a.id FROM a SEMI JOIN b", + expected_code: "SQC003", + expected_replacement: None, + }, + test_types::LintFixTestCase { + description: "redundant distinct", + sql: "SELECT DISTINCT id FROM items GROUP BY id", + expected_code: "SQC006", + expected_replacement: Some(""), + }, + test_types::LintFixTestCase { + description: "single unused select CTE", + sql: "WITH unused AS (SELECT 1) SELECT 1", + expected_code: "SQC005", + expected_replacement: Some(""), + }, + test_types::LintFixTestCase { + description: "reserved framework CTE is never deleted", + sql: "WITH __expected__items AS (SELECT 1) SELECT 1", + expected_code: "SQC005", + expected_replacement: None, + }, + ]; + + for test_case in test_cases { + let diagnostics = helpers::diagnostics(test_case.sql)?; + let diagnostic = diagnostics + .iter() + .find(|item| item["code"] == test_case.expected_code) + .ok_or_else(|| format!("{} should report", test_case.description))?; + assert_eq!( + diagnostic["fix"]["replacement"].as_str(), + test_case.expected_replacement, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_additional_rule_cases_when_linting_then_findings_and_fixes_match() -> Result<(), String> { + let test_cases = [ + test_types::AdditionalLintRuleTestCase { + description: "bare union", + sql: "SELECT 1 UNION SELECT 2", + rule: "SQC008", + expected_anchor: Some("UNION"), + expected_replacement: Some("UNION DISTINCT"), + }, + test_types::AdditionalLintRuleTestCase { + description: "duplicate relation alias", + sql: "SELECT x.id FROM alpha AS x JOIN beta AS x ON x.id = x.id", + rule: "SQC009", + expected_anchor: Some("x"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "duplicate output alias", + sql: "SELECT a AS value, b AS value FROM items", + rule: "SQC010", + expected_anchor: Some("value"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "mixed grouping references", + sql: "SELECT a, b FROM items GROUP BY 1, b", + rule: "SQC011", + expected_anchor: Some("GROUP"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "redundant else null", + sql: "SELECT CASE WHEN a THEN b ELSE NULL END FROM items", + rule: "SQC012", + expected_anchor: Some("ELSE NULL"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "function-like distinct", + sql: "SELECT DISTINCT(a) FROM items", + rule: "SQC013", + expected_anchor: Some("DISTINCT(a)"), + expected_replacement: Some("DISTINCT a"), + }, + test_types::AdditionalLintRuleTestCase { + description: "constant scaffold predicate", + sql: "SELECT a FROM items WHERE 1 = 1", + rule: "SQC014", + expected_anchor: Some("="), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "consecutive statement terminator", + sql: "SELECT 1;;", + rule: "SQC015", + expected_anchor: Some(";"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "redundant self alias", + sql: "SELECT value AS value FROM items", + rule: "SQC016", + expected_anchor: Some(" AS value"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified column self alias is redundant", + sql: "SELECT items.value AS value FROM items", + rule: "SQC016", + expected_anchor: Some(" AS value"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified column self alias after an expression is redundant", + sql: "SELECT item_id + 1 AS next_item_id, items.value AS value FROM items", + rule: "SQC016", + expected_anchor: Some(" AS value"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "top-modified column self alias is redundant", + sql: "SELECT TOP 5 value AS value FROM items", + rule: "SQC016", + expected_anchor: Some(" AS value"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "terminal operand name does not make a calculation self-aliased", + sql: "SELECT price + value AS value FROM items", + rule: "SQC016", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "cast type syntax is not an output self alias", + sql: "SELECT CAST(order_id::INTEGER AS INTEGER) AS order_id FROM orders", + rule: "SQC016", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "relation self alias is not an output self alias", + sql: "SELECT orders.order_id FROM orders AS orders", + rule: "SQC016", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "semi-structured path alias is not redundant", + sql: "SELECT event.payload:data:order_id AS order_id FROM events AS event", + rule: "SQC016", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "constant row count", + sql: "SELECT COUNT(1) FROM items", + rule: "SQC017", + expected_anchor: Some("1"), + expected_replacement: Some("*"), + }, + test_types::AdditionalLintRuleTestCase { + description: "unordered row number", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY id) FROM items", + rule: "SQC018", + expected_anchor: Some("ROW_NUMBER"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "literal null in not-in list", + sql: "SELECT id FROM items WHERE id NOT IN (1, NULL)", + rule: "SQC019", + expected_anchor: Some("NOT"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "set branch arity mismatch", + sql: "SELECT a, b FROM first UNION ALL SELECT c FROM second", + rule: "SQC020", + expected_anchor: Some("UNION"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "uncontrolled projection star", + sql: "SELECT * FROM items", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "projection star without from remains uncontrolled", + sql: "SELECT *", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "mixed dynamic pivot projection remains uncontrolled", + sql: "SELECT *, customer_id FROM orders PIVOT(MAX(amount) FOR category IN (ANY))", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "static pivot star remains uncontrolled", + sql: "SELECT * FROM orders PIVOT(MAX(amount) FOR category IN ('books'))", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "static pivot any string remains uncontrolled", + sql: "SELECT * FROM orders PIVOT(MAX(amount) FOR category IN ('ANY'))", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "nested dynamic pivot does not exempt outer projection star", + sql: "SELECT * FROM orders WHERE EXISTS (SELECT 1 FROM items PIVOT(MAX(amount) FOR category IN (ANY)))", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unrelated any does not exempt static pivot star", + sql: "SELECT * FROM orders PIVOT(MAX(amount) FOR category IN ('books')) WHERE customer_id = ANY (SELECT customer_id FROM customers)", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "comma joined dynamic pivot star remains uncontrolled", + sql: "SELECT * FROM orders PIVOT(MAX(amount) FOR category IN (ANY)) AS p, customers AS c", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "joined audit relation parameter star remains uncontrolled", + sql: "SELECT * FROM __sqlbuild_audit_parameter_0__ AS a CROSS JOIN customers AS c", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "terminal framework dependency star remains uncontrolled", + sql: "SELECT * FROM __sqb_lint_0__", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "transformed framework dependency import star remains uncontrolled", + sql: "WITH imported_orders AS (SELECT *, CURRENT_TIMESTAMP AS loaded_at FROM __sqb_lint_0__) SELECT order_id FROM imported_orders", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "ordinary relation import star remains uncontrolled", + sql: "WITH imported_orders AS (SELECT * FROM orders) SELECT order_id FROM imported_orders", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "nested framework dependency import star remains uncontrolled", + sql: "WITH outer_query AS (WITH imported_orders AS (SELECT * FROM __sqb_lint_0__) SELECT order_id FROM imported_orders) SELECT order_id FROM outer_query", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased calculated projection", + sql: "SELECT price * quantity FROM items", + rule: "SQC022", + expected_anchor: Some("price"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unused table alias", + sql: "SELECT id FROM items AS unused", + rule: "SQC023", + expected_anchor: Some("AS unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "left join rejected in where", + sql: "SELECT a.id FROM a LEFT JOIN b AS right_side ON a.id = right_side.id WHERE right_side.active = TRUE", + rule: "SQC024", + expected_anchor: Some("LEFT"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit inner join", + sql: "SELECT a.id FROM a JOIN b ON a.id = b.id", + rule: "SQC025", + expected_anchor: Some("JOIN"), + expected_replacement: Some("INNER JOIN"), + }, + test_types::AdditionalLintRuleTestCase { + description: "mixed order directions", + sql: "SELECT a, b FROM items ORDER BY a, b DESC", + rule: "SQC026", + expected_anchor: Some("ORDER"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unqualified multi-source column", + sql: "SELECT id FROM a JOIN b ON a.id = b.id", + rule: "SQC027", + expected_anchor: Some("id"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "mixed single-source qualification", + sql: "SELECT items.id, name FROM items", + rule: "SQC028", + expected_anchor: Some("name"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "plain macro slot reads count as unqualified beside a plain group read", + sql: "SELECT __sqlbuild_macro_parameter_1__, __sqlbuild_macro_parameter_0__.__sqlbuild_macro_parameter_2__ AS orders FROM __sqlbuild_macro_parameter_0__", + rule: "SQC028", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "a plain macro slot read beside a hand-qualified column is mixed", + sql: "SELECT __sqlbuild_macro_parameter_0__.__sqlbuild_macro_parameter_1__ AS orders, s.refunds FROM __sqlbuild_macro_parameter_0__ AS s", + rule: "SQC028", + expected_anchor: Some("__sqlbuild_macro_parameter_0__"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unknown relation qualifier", + sql: "SELECT missing.id FROM items AS present", + rule: "SQC029", + expected_anchor: Some("missing"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake shorthand cast type is not an unqualified column", + sql: "SELECT src.value::STRING AS value FROM items AS src", + rule: "SQC028", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake JSON path and shorthand cast type are not columns", + sql: "SELECT src.payload:item::TEXT AS item FROM items AS src", + rule: "SQC028", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake shorthand cast type is not a multi-source column", + sql: "SELECT first.value::TEXT AS value FROM first JOIN second ON first.id = second.id", + rule: "SQC027", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit table alias is known in query scope", + sql: "SELECT present.id FROM items present", + rule: "SQC029", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit joined alias contributes a selected value", + sql: "SELECT left_side.id, right_side.name FROM alpha left_side LEFT JOIN beta right_side ON left_side.id = right_side.id", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake lateral flatten comma is explicit lateral syntax", + sql: "SELECT flattened.value FROM items, LATERAL FLATTEN(input => items.payload) flattened", + rule: "SQC002", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake VALUES row separator is not a relation separator", + sql: "SELECT column1 FROM VALUES (1), (2), (3)", + rule: "SQC002", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "arithmetic multiplication is not a projected wildcard", + sql: "SELECT price * quantity AS total FROM items", + rule: "SQC021", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "number-led multiplication is not a projected wildcard", + sql: "SELECT 100 * ratio AS percentage FROM items", + rule: "SQC021", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "TOP projection wildcard remains controlled by star rule", + sql: "SELECT TOP 10 * FROM items", + rule: "SQC021", + expected_anchor: Some("*"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified wildcard modifier does not require an alias", + sql: "SELECT src.* EXCLUDE (internal_id) FROM items AS src", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "EXISTS literal projection does not require an alias", + sql: "SELECT parent.id FROM parent WHERE EXISTS (SELECT 1 FROM child WHERE child.id = parent.id)", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "correlated subquery can reference an outer relation", + sql: "SELECT parent.id FROM parent WHERE EXISTS (SELECT 1 FROM child WHERE child.id = parent.id)", + rule: "SQC029", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake JSON path key is not a relation qualifier", + sql: "SELECT PARSE_JSON(src.payload):item.name::VARCHAR AS item_name FROM items AS src", + rule: "SQC029", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake JSON path starts from a qualified relation value", + sql: "SELECT parsed:item::VARCHAR AS item FROM items AS src, LATERAL FLATTEN(input => src.payload) AS parsed", + rule: "SQC027", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "Snowflake ASOF JOIN alias is known in query scope", + sql: "SELECT matched.ts FROM requested request ASOF JOIN available matched MATCH_CONDITION (request.ts >= matched.ts) ON request.id = matched.id", + rule: "SQC029", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "later set branches inherit the first branch output name", + sql: "SELECT 0 AS offset_value UNION ALL SELECT 1 UNION ALL SELECT 2", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "scalar subquery output does not require an alias inside its scope", + sql: "SELECT (SELECT MAX(child.value) FROM child WHERE child.id = parent.id) AS maximum_value FROM parent", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "later scalar function argument does not require an inner alias", + sql: "SELECT COALESCE((SELECT MAX(change_id) FROM first_changes), (SELECT MAX(change_id) FROM second_changes), 0) AS latest_change_id FROM support_tickets", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "scalar aggregate nested in conditional coalesce does not require an inner alias", + sql: "SELECT CASE WHEN batch.layout = 'fallback' THEN COALESCE((SELECT MAX(change.line_id) FROM first_changes AS change WHERE change.batch_id = batch.batch_id AND change.line_id < batch.line_id), (SELECT MAX(archive.line_id) FROM archived_changes AS archive WHERE archive.batch_id = batch.batch_id AND archive.line_id < batch.line_id), header.line_id) + 1 ELSE batch.start_line END AS start_line FROM support_batches AS batch LEFT JOIN support_headers AS header ON header.batch_id = batch.batch_id", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "prior nested relation does not contaminate a later scalar function argument", + sql: "WITH prior_changes AS (SELECT (SELECT MAX(change_id) FROM support_changes) AS latest_change_id FROM support_tickets), current_changes AS (SELECT COALESCE((SELECT MAX(change_id) FROM first_changes), (SELECT MAX(change_id) FROM second_changes), 0) AS latest_change_id FROM current_tickets) SELECT * FROM current_changes", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "scalar subquery after a prior CTE relation remains in projection scope", + sql: "WITH products_used AS (SELECT product_id FROM products), ticket_totals AS (SELECT ticket.ticket_id, (SELECT MAX(change_id) FROM support_changes) AS latest_change_id FROM support_tickets AS ticket) SELECT * FROM ticket_totals", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "comma-derived table calculation still requires an alias", + sql: "SELECT derived.value FROM items, (SELECT amount + tax FROM totals) AS derived", + rule: "SQC022", + expected_anchor: Some("amount"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "scalar subquery scope does not cross a prior statement", + sql: "SELECT order_id FROM orders; SELECT invoice_id, (SELECT MAX(amount + tax) FROM totals) AS maximum_total FROM invoices", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "contextual keyword remains a simple qualified projection", + sql: "SELECT flattened.index FROM items AS src, LATERAL FLATTEN(input => src.payload) AS flattened", + rule: "SQC022", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "generated expression sentinel is not an authored column", + sql: "SELECT first.id FROM first JOIN second ON first.id = second.id WHERE __sqb_lint_0__", + rule: "SQC027", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "null-safe comparison FROM is not a relation clause", + sql: "SELECT CASE WHEN src.actual IS DISTINCT FROM src.expected THEN 1 ELSE 0 END AS differs FROM items AS src", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "earlier projection alias is not a relation column", + sql: "SELECT flattened.value AS item_index, item_index + 1 AS next_index FROM items AS src, LATERAL FLATTEN(input => src.payload) AS flattened", + rule: "SQC027", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "current date context value is not a multi-source column", + sql: "SELECT first_side.id FROM first_side JOIN second_side ON first_side.id = second_side.id WHERE first_side.created_at >= CURRENT_DATE", + rule: "SQC027", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "current date context value does not mix qualification styles", + sql: "SELECT first_side.id FROM first_side WHERE first_side.created_at >= CURRENT_DATE", + rule: "SQC028", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "simple boolean case", + sql: "SELECT CASE WHEN amount > 0 THEN TRUE ELSE FALSE END AS positive FROM items", + rule: "SQC030", + expected_anchor: Some("CASE WHEN amount > 0 THEN TRUE ELSE FALSE END"), + expected_replacement: Some("COALESCE(amount > 0, FALSE)"), + }, + test_types::AdditionalLintRuleTestCase { + description: "unicode before simple boolean case preserves replacement text", + sql: "SELECT 'é', CASE WHEN amount > 0 THEN TRUE ELSE FALSE END AS positive FROM items", + rule: "SQC030", + expected_anchor: Some("CASE WHEN amount > 0 THEN TRUE ELSE FALSE END"), + expected_replacement: Some("COALESCE(amount > 0, FALSE)"), + }, + test_types::AdditionalLintRuleTestCase { + description: "commented boolean case is diagnosed without a fix", + sql: "SELECT CASE WHEN amount /* reason */ > 0 THEN TRUE ELSE FALSE END FROM items", + rule: "SQC030", + expected_anchor: Some("CASE WHEN amount /* reason */ > 0 THEN TRUE ELSE FALSE END"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "multi-branch boolean case is diagnosed without an unsafe partial fix", + sql: "SELECT CASE WHEN status = 'open' THEN TRUE WHEN priority = 'high' THEN TRUE ELSE FALSE END AS actionable FROM support_tickets", + rule: "SQC030", + expected_anchor: Some( + "CASE WHEN status = 'open' THEN TRUE WHEN priority = 'high' THEN TRUE ELSE FALSE END", + ), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "case nested directly in else", + sql: "SELECT CASE WHEN a THEN 1 ELSE CASE WHEN b THEN 2 END END FROM items", + rule: "SQC031", + expected_anchor: Some("CASE"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "joined relation contributes no values", + sql: "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id", + rule: "SQC032", + expected_anchor: Some("JOIN"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "joined relation contributes through an aggregate expression", + sql: "SELECT ARRAY_AGG(b.value) AS values FROM a LEFT JOIN b ON a.id = b.id", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "bare projected star consumes every joined relation", + sql: "SELECT * FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.customer_id", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unqualified projection prevents an unsupported unused relation claim", + sql: "SELECT customer_name FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.customer_id", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unqualified aggregate argument prevents an unsupported unused relation claim", + sql: "SELECT MAX(customer_name) FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.customer_id", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "left qualified star does not consume joined relation", + sql: "SELECT o.* FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.customer_id", + rule: "SQC032", + expected_anchor: Some("JOIN"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "cross joined relation contributes through a filter predicate", + sql: "SELECT orders.order_id FROM orders CROSS JOIN processing_cutoff AS cutoff WHERE orders.created_at < cutoff.created_at", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "lateral relation contributes through a downstream join condition", + sql: "SELECT orders.order_id, products.name FROM orders CROSS JOIN LATERAL UNNEST(orders.product_ids) AS item INNER JOIN products ON (products.product_id = item.product_id)", + rule: "SQC032", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit union is clean", + sql: "SELECT 1 UNION ALL SELECT 2", + rule: "SQC008", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "ordered row number without a declared key is unproven", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY id ORDER BY created_at, id) FROM items", + rule: "SQC018", + expected_anchor: Some("ROW_NUMBER"), + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "equal set arity is clean", + sql: "SELECT a, b FROM first UNION ALL SELECT c, d FROM second", + rule: "SQC020", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "dynamic pivot star has data-dependent columns", + sql: "SELECT * FROM orders PIVOT(MAX(amount) FOR category IN (ANY ORDER BY category))", + rule: "SQC021", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "audit relation parameter star has invocation-dependent columns", + sql: "SELECT * FROM __sqlbuild_audit_parameter_0__", + rule: "SQC021", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified alias is used", + sql: "SELECT kept.id FROM items AS kept", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "left join predicate retained in on is clean", + sql: "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id AND b.active = TRUE", + rule: "SQC024", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit inner join is clean", + sql: "SELECT a.id FROM a INNER JOIN b ON a.id = b.id", + rule: "SQC025", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "null literal inside not-in subquery expression is not a literal list", + sql: "SELECT id FROM items WHERE id NOT IN (SELECT COALESCE(id, NULL) FROM other)", + rule: "SQC019", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "left join null probe preserves unmatched rows", + sql: "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id WHERE b.id IS NULL", + rule: "SQC024", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "left join predicate guarded by null probe preserves unmatched rows", + sql: "SELECT a.id FROM a LEFT JOIN b ON a.id = b.id WHERE b.active = TRUE OR b.id IS NULL", + rule: "SQC024", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified catalog path is not treated as an unknown relation alias", + sql: "SELECT catalog.schema.items.id FROM catalog.schema.items", + rule: "SQC029", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "correlated subquery keeps outer table alias", + sql: "SELECT id FROM items AS kept WHERE EXISTS (SELECT 1 FROM other WHERE other.id = kept.id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "asof join is not rewritten as an inner join", + sql: "SELECT a.id FROM a ASOF JOIN b ON a.id = b.id", + rule: "SQC025", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "semi join is not rewritten as an inner join", + sql: "SELECT a.id FROM a SEMI JOIN b ON a.id = b.id", + rule: "SQC025", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "table alias carrying a column list is retained", + sql: "SELECT a, b FROM items AS t(a, b)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "bracket list commas do not change set projection arity", + sql: "SELECT a, [1, 2, 3] AS arr FROM t UNION ALL SELECT a, [4] AS arr FROM t", + rule: "SQC020", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "quoted output alias is not redundant with an unquoted identifier", + sql: "SELECT value AS \"value\" FROM items", + rule: "SQC016", + expected_anchor: None, + expected_replacement: None, + }, + ]; + + for test_case in test_cases { + let diagnostics = helpers::diagnostics_for_rules(test_case.sql, &[test_case.rule])?; + assert_eq!( + diagnostics.len(), + usize::from(test_case.expected_anchor.is_some()), + "{}", + test_case.description + ); + let _ = test_case.expected_anchor.map(|expected_anchor| { + let diagnostic = &diagnostics[0]; + let start = diagnostic["start"].as_u64().unwrap_or_default() as usize; + let end = diagnostic["end"].as_u64().unwrap_or_default() as usize; + let source: String = test_case + .sql + .chars() + .skip(start) + .take(end - start) + .collect(); + assert_eq!(source, expected_anchor, "{}", test_case.description); + assert_eq!( + diagnostic["fix"]["replacement"].as_str(), + test_case.expected_replacement, + "{}", + test_case.description + ); + }); + } + Ok(()) +} + +#[test] +fn given_ceremonial_cte_contexts_when_linting_then_only_harness_dependencies_are_reachable() +-> Result<(), String> { + let test_cases = [ + test_types::CeremonialCteLintTestCase { + description: "macro harness outputs keep their helper dependency reachable", + sql: "WITH input_values AS (SELECT 7 AS amount), __macro_actual__ AS (SELECT amount FROM input_values), __macro_expected__ AS (SELECT 7 AS amount) SELECT 1", + expected_count: 0, + expected_start: None, + }, + test_types::CeremonialCteLintTestCase { + description: "non-harness double-underscore CTE remains diagnosable", + sql: "WITH __scratch AS (SELECT 1), __macro_actual__ AS (SELECT 7 AS amount), __macro_expected__ AS (SELECT 7 AS amount) SELECT 1", + expected_count: 1, + expected_start: Some(5), + }, + ]; + for test_case in test_cases { + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "duckdb", + "enabled_rules": ["SQC005"], + "allows_ceremonial_select": true, + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + let diagnostics = payload["diagnostics"] + .as_array() + .ok_or_else(|| "diagnostics should be an array".to_string())?; + + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + assert_eq!( + diagnostics + .first() + .and_then(|diagnostic| diagnostic["start"].as_u64()), + test_case.expected_start, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_dependency_identity_when_linting_import_stars_then_only_plain_top_level_import_is_accepted() +-> Result<(), String> { + let test_cases = [ + test_types::DependencyImportLintTestCase { + description: "plain top-level dependency import", + sql: "WITH imported_orders AS (SELECT * FROM __sqb_lint_0__) SELECT order_id FROM imported_orders", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 0, + }, + test_types::DependencyImportLintTestCase { + description: "star without from remains diagnosable", + sql: "WITH imported_orders AS (SELECT *) SELECT order_id FROM imported_orders", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 1, + }, + test_types::DependencyImportLintTestCase { + description: "set operation import is transformed", + sql: "WITH imported_orders AS (SELECT * FROM __sqb_lint_0__ UNION ALL SELECT order_id FROM orders) SELECT order_id FROM imported_orders", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 1, + }, + test_types::DependencyImportLintTestCase { + description: "dynamic relation is not a dependency", + sql: "WITH imported_orders AS (SELECT * FROM __sqb_lint_0__) SELECT order_id FROM imported_orders", + dependency_identifiers: &[], + expected_count: 1, + }, + test_types::DependencyImportLintTestCase { + description: "terminal dependency star remains uncontrolled", + sql: "SELECT * FROM __sqb_lint_0__", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 1, + }, + test_types::DependencyImportLintTestCase { + description: "transformed dependency import remains uncontrolled", + sql: "WITH imported_orders AS (SELECT *, CURRENT_TIMESTAMP AS loaded_at FROM __sqb_lint_0__) SELECT order_id FROM imported_orders", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 1, + }, + test_types::DependencyImportLintTestCase { + description: "nested dependency import remains uncontrolled", + sql: "WITH outer_query AS (WITH imported_orders AS (SELECT * FROM __sqb_lint_0__) SELECT order_id FROM imported_orders) SELECT order_id FROM outer_query", + dependency_identifiers: &["__sqb_lint_0__"], + expected_count: 1, + }, + ]; + for test_case in test_cases { + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "duckdb", + "enabled_rules": ["SQC021"], + "dependency_identifiers": test_case.dependency_identifiers, + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + let diagnostics = payload["diagnostics"] + .as_array() + .ok_or_else(|| "diagnostics should be an array".to_owned())?; + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_ceremonial_select_context_when_linting_terminal_shape_then_rule_035_allows_it() +-> Result<(), String> { + let test_cases = [test_types::PlainSqlLintTestCase { + description: "direct ceremonial select is valid in harness context", + sql: "WITH __source__raw_orders AS (SELECT 1 AS order_id), __expected__orders AS (SELECT 1 AS order_id) SELECT 1", + rule: "SQC035", + expected_count: 0, + }]; + for test_case in test_cases { + let response = lint_json( + &json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "duckdb", + "enabled_rules": [test_case.rule], + "allows_ceremonial_select": true, + }) + .to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + let diagnostics = payload["diagnostics"] + .as_array() + .ok_or_else(|| "diagnostics should be an array".to_owned())?; + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_tsql_bare_union_when_linting_then_diagnostic_has_no_invalid_fix() -> Result<(), String> { + let test_cases = [test_types::DialectLintRuleTestCase { + description: "T-SQL bare UNION is diagnosed without an invalid fix", + sql: "SELECT 1 UNION SELECT 2", + dialect: "tsql", + rule: "SQC008", + expected_count: 1, + expected_fix: false, + expected_reason: "the active dialect does not accept explicit UNION DISTINCT", + }]; + + for test_case in test_cases { + let diagnostics = + helpers::diagnostics_for_dialect(test_case.sql, test_case.dialect, &[test_case.rule])?; + assert_eq!( + diagnostics.len(), + test_case.expected_count, + "{}", + test_case.description + ); + assert_eq!( + diagnostics[0].get("fix").is_some(), + test_case.expected_fix, + "{}", + test_case.description + ); + assert_eq!( + diagnostics[0]["fix_unavailable_reason"], test_case.expected_reason, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_format_cases_when_formatting_then_output_matches() -> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "comment-free canonical SQL", + sql: "select a,b from items where a=1", + expected_sql: "SELECT\n a,\n b\nFROM items\nWHERE a = 1", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "lowercase function canonicalization preserves structure", + sql: "select count(1) from items", + expected_sql: "SELECT COUNT(1)\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "block comment attachment", + sql: "SELECT a, /* preserve */ b FROM items", + expected_sql: "SELECT\n a, /* preserve */\n b\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "line and leading comments", + sql: "-- lead\nselect a,b from items -- tail\nwhere a=1", + expected_sql: "-- lead\nSELECT\n a,\n b\nFROM items -- tail\nWHERE a = 1", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "standalone projection comments remain attached to following expressions", + sql: "SELECT\n -- first value\n a,\n -- second value\n b\nFROM items", + expected_sql: "SELECT\n -- first value\n a,\n -- second value\n b\nFROM items", + expected_changed: false, + }, + test_types::FormatTestCase { + description: "comment marker string", + sql: "select '-- not a comment' as value", + expected_sql: "SELECT '-- not a comment' AS value", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "doubled apostrophe string remains compiler compatible", + sql: "select 'Customer''s order' as value", + expected_sql: "SELECT 'Customer''s order' AS value", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "explicit null ordering remains explicit", + sql: "select id from items order by category nulls last, created_at desc nulls first", + expected_sql: "SELECT id\nFROM items\nORDER BY\n category NULLS LAST,\n created_at DESC NULLS FIRST", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "explicit cast type spellings remain authored", + sql: "select cast(label as TEXT) as label, cast(id as INTEGER) as id, cast(observed_at as TIMESTAMP_NTZ) as observed_at, cast(amount as NUMERIC(10, 2)) as amount from items", + expected_sql: "SELECT\n CAST(label AS TEXT) AS label,\n CAST(id AS INTEGER) AS id,\n CAST(observed_at AS TIMESTAMP_NTZ) AS observed_at,\n CAST(amount AS NUMERIC(10, 2)) AS amount\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "nested and try cast type spellings remain authored", + sql: "select cast(coalesce(try_cast(raw_id as INTEGER), 0) as NUMERIC(10, 2)) as amount, cast(tags as ARRAY) as tags from items", + expected_sql: "SELECT\n CAST(COALESCE(TRY_CAST(raw_id AS INTEGER), 0) AS NUMERIC(10, 2)) AS amount,\n CAST(tags AS ARRAY) AS tags\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "commented cast type remains idempotent", + sql: "select cast(amount as NUMERIC(/* precision */ 10, 2)) as amount from items", + expected_sql: "SELECT\n CAST(amount AS NUMERIC( /* precision */10, 2)) AS amount\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "cast-free token expansion remains supported", + sql: "select id label from items", + expected_sql: "SELECT id label\nFROM items", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "cast-free multiple statements remain supported", + sql: "select 1; select 2", + expected_sql: "SELECT 1;\nSELECT 2", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "trailing comment after a kept statement terminator", + sql: "select a from items; -- retained", + expected_sql: "SELECT a\nFROM items; -- retained", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "unparenthesized values relation remains unparenthesized", + sql: "select column1::varchar as customer_key, column2::integer as order_count from values ('c1', 1), ('c2', 2)", + expected_sql: "SELECT\n column1::varchar AS customer_key,\n column2::integer AS order_count\nFROM VALUES ('c1', 1), ('c2', 2)", + expected_changed: true, + }, + ]; + + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "snowflake"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_ordered_sql_batch_when_formatting_then_outputs_are_parallel_and_ordered() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "first order query", + sql: "select order_id from orders where order_id=1", + expected_sql: "SELECT order_id\nFROM orders\nWHERE order_id = 1", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "second product query", + sql: "select product_id from products where product_id=2", + expected_sql: "SELECT product_id\nFROM products\nWHERE product_id = 2", + expected_changed: true, + }, + ]; + let requests: Vec = test_cases + .iter() + .map(|test_case| json!({"version": 1, "sql": test_case.sql, "dialect": "snowflake"})) + .collect(); + + let response = + format_batch_json(&serde_json::to_string(&requests).map_err(|error| error.to_string())?)?; + let payloads: Vec = + serde_json::from_str(&response).map_err(|error| error.to_string())?; + + assert_eq!(payloads.len(), test_cases.len()); + let mut payload_iter = payloads.iter(); + for test_case in &test_cases { + let payload = payload_iter + .next() + .ok_or_else(|| format!("{}: batch response omitted an item", test_case.description))?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!(payload["changed"], test_case.expected_changed); + } + Ok(()) +} + +#[test] +fn given_unknown_rule_when_linting_then_request_is_rejected() -> Result<(), String> { + let test_cases = [test_types::InvalidRuleTestCase { + description: "unknown native lint rule", + rule: "SQC999", + expected_message: "unknown native lint rule 'SQC999'", + }]; + + for test_case in &test_cases { + let request = json!({"version": 1, "sql": "SELECT 1", "dialect": "snowflake", "enabled_rules": [test_case.rule]}).to_string(); + let error = lint_json(&request) + .err() + .ok_or_else(|| "unknown rule should fail".to_string())?; + assert!( + error.contains(test_case.expected_message), + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_types.rs b/crates/sqlclean/src/sql_lint/tests/test_types.rs new file mode 100644 index 0000000..6a3348b --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_types.rs @@ -0,0 +1,182 @@ +pub(crate) struct LintTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_codes: &'static [&'static str], + pub expected_anchors: &'static [(&'static str, &'static str)], +} + +pub(crate) struct EmptyFixtureLintTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) allows_empty_fixture_star: bool, + pub(crate) expected_diagnostic_count: usize, +} + +pub(crate) struct BatchLintTestCase { + pub(crate) description: &'static str, + pub(crate) queries: &'static [&'static str], + pub(crate) expected_responses: usize, +} + +pub(crate) struct LintMetadataTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_code: &'static str, + pub expected_message: &'static str, + pub expected_remediation: &'static str, +} + +pub(crate) struct LintFixTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_code: &'static str, + pub expected_replacement: Option<&'static str>, +} + +pub(crate) struct AdditionalLintRuleTestCase { + pub description: &'static str, + pub sql: &'static str, + pub rule: &'static str, + pub expected_anchor: Option<&'static str>, + pub expected_replacement: Option<&'static str>, +} + +pub(crate) struct CeremonialCteLintTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_count: usize, + pub expected_start: Option, +} + +pub(crate) struct PlainSqlLintTestCase { + pub description: &'static str, + pub sql: &'static str, + pub rule: &'static str, + pub expected_count: usize, +} + +pub(crate) struct SqlMacroOutputLintTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + /// The text a `@sql.` call generated, and the names the call passed. + pub(crate) output: &'static str, + pub(crate) relations: &'static [&'static str], + pub(crate) expected_count: usize, +} + +pub(crate) struct DynamicOutputStarLintTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) expected_count: usize, + pub(crate) expected_prefix: &'static str, +} + +pub(crate) struct DependencyImportLintTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) dependency_identifiers: &'static [&'static str], + pub(crate) expected_count: usize, +} + +pub(crate) struct DialectLintRuleTestCase { + pub description: &'static str, + pub sql: &'static str, + pub dialect: &'static str, + pub rule: &'static str, + pub expected_count: usize, + pub expected_fix: bool, + pub expected_reason: &'static str, +} + +pub(crate) struct LineWrapTestCase { + pub description: &'static str, + pub sql: &'static str, + pub line_width: usize, + pub token_widths: &'static [(&'static str, usize)], + pub expected_sql: &'static str, +} + +pub(crate) struct FunctionCallSpacingTestCase { + pub description: &'static str, + pub dialect: &'static str, + pub line_width: usize, + pub argument: &'static str, + pub expected_refused: &'static [&'static str], +} + +pub(crate) struct LayoutRuleTestCase { + pub description: &'static str, + pub dialect: &'static str, + pub sql: &'static str, + pub line_width: usize, + pub expected_sql: &'static str, +} + +pub(crate) struct FormatCorpusTestCase { + pub description: &'static str, + pub dialect: &'static str, + pub expected_formatted: usize, + pub expected_refused: usize, +} + +pub(crate) struct DialectFormatTestCase { + pub description: &'static str, + pub dialect: &'static str, + pub sql: &'static str, + pub expected_sql: &'static str, +} + +pub(crate) struct TokenInvariantTestCase { + pub description: &'static str, + pub before: &'static str, + pub after: &'static str, + pub recases: &'static [(usize, &'static str, bool)], + pub expected_error: Option<&'static str>, +} + +pub(crate) struct FormatTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_sql: &'static str, + pub expected_changed: bool, +} + +pub(crate) struct ReprintTestCase { + pub description: &'static str, + pub sql: &'static str, + pub expected_changed: bool, + pub expected_reprints: usize, +} + +pub(crate) struct InvalidRuleTestCase { + pub description: &'static str, + pub rule: &'static str, + pub expected_message: &'static str, +} + +pub(crate) struct FunctionDepthTestCase { + pub description: &'static str, + pub depth: usize, + pub expected_diagnostic_count: usize, +} + +pub(crate) struct FunctionDepthFailureTestCase { + pub description: &'static str, + pub depth: usize, + pub expected_code: &'static str, + pub expected_limit: &'static str, +} + +pub(crate) struct CrossJoinTestCase { + pub description: &'static str, + pub sql: &'static str, + pub dialect: &'static str, + pub one_row_relations: &'static [&'static str], + pub expected_messages: &'static [&'static str], +} + +pub(crate) struct CrossJoinSubjectTestCase { + pub description: &'static str, + pub message: &'static str, + pub expected_subject: Option<(&'static str, Option<&'static str>)>, +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_unused_table_alias.rs b/crates/sqlclean/src/sql_lint/tests/test_unused_table_alias.rs new file mode 100644 index 0000000..95a86de --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_unused_table_alias.rs @@ -0,0 +1,317 @@ +use crate::sql_lint::tests::{helpers, test_types}; + +#[test] +fn given_implicit_and_explicit_table_aliases_when_linting_then_unused_aliases_match() +-> Result<(), String> { + let test_cases = [ + test_types::AdditionalLintRuleTestCase { + description: "unused implicit table alias", + sql: "SELECT id FROM items unused", + rule: "SQC023", + expected_anchor: Some("unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "unused implicit joined table alias", + sql: "SELECT o.id FROM orders o INNER JOIN customers unused ON o.customer_id = o.id", + rule: "SQC023", + expected_anchor: Some("unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "unused implicit qualified table alias", + sql: "SELECT id FROM warehouse.analytics.items unused", + rule: "SQC023", + expected_anchor: Some("unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "unused implicit subquery alias", + sql: "SELECT id FROM (SELECT id FROM items) unused", + rule: "SQC023", + expected_anchor: Some("unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "unused implicit lateral table function alias", + sql: "SELECT src.id FROM items src, LATERAL FLATTEN(input => src.payload) unused", + rule: "SQC023", + expected_anchor: Some("unused"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "used implicit table alias", + sql: "SELECT kept.id FROM items kept", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "used implicit joined table aliases", + sql: "SELECT o.id, c.name FROM orders o INNER JOIN customers c ON o.customer_id = c.id", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "used implicit qualified table alias", + sql: "SELECT kept.id FROM warehouse.analytics.items kept", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "used implicit subquery alias", + sql: "SELECT sub.id FROM (SELECT id FROM items) sub", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "used implicit lateral table function alias", + sql: "SELECT flattened.value FROM items src, LATERAL FLATTEN(input => src.payload) flattened", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias used by correlated subquery", + sql: "SELECT id FROM items kept WHERE EXISTS (SELECT 1 FROM other WHERE other.id = kept.id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias carrying a column list is retained", + sql: "SELECT a FROM items t(a)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "qualified relation name is not an implicit alias", + sql: "SELECT id FROM warehouse.analytics.items", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "join modifier keywords are not implicit aliases", + sql: "SELECT o.id, c.name FROM orders o POSITIONAL JOIN customers c", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "join condition identifiers are not implicit aliases", + sql: "SELECT o.id, c.name FROM orders o INNER JOIN customers c USING (customer_id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + ]; + + for test_case in test_cases { + let diagnostics = helpers::diagnostics_for_rules(test_case.sql, &[test_case.rule])?; + assert_eq!( + diagnostics.len(), + usize::from(test_case.expected_anchor.is_some()), + "{}", + test_case.description + ); + let _ = test_case.expected_anchor.map(|expected_anchor| { + let diagnostic = &diagnostics[0]; + let start = diagnostic["start"].as_u64().unwrap_or_default() as usize; + let end = diagnostic["end"].as_u64().unwrap_or_default() as usize; + assert_eq!( + &test_case.sql[start..end], + expected_anchor, + "{}", + test_case.description + ); + assert_eq!( + diagnostic["fix"]["replacement"].as_str(), + test_case.expected_replacement, + "{}", + test_case.description + ); + }); + } + Ok(()) +} + +#[test] +fn given_join_modifiers_and_whole_row_uses_when_linting_duckdb_then_aliases_are_kept() +-> Result<(), String> { + let test_cases = [ + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before positional join has no alias", + sql: "SELECT * FROM orders POSITIONAL JOIN customers", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before asof join has no alias", + sql: "SELECT * FROM orders ASOF JOIN prices ON orders.item_id = prices.item_id AND orders.ordered_at >= prices.valid_from", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before natural join has no alias", + sql: "SELECT * FROM orders NATURAL JOIN customers", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before anti join has no alias", + sql: "SELECT * FROM orders ANTI JOIN refunds USING (order_id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before semi join has no alias", + sql: "SELECT * FROM orders SEMI JOIN refunds USING (order_id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before cross join has no alias", + sql: "SELECT * FROM orders CROSS JOIN customers", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias repeating the relation name is unused", + sql: "SELECT id FROM orders orders", + rule: "SQC023", + expected_anchor: Some("orders"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit alias repeating the relation name is unused", + sql: "SELECT id FROM orders AS orders", + rule: "SQC023", + expected_anchor: Some("AS orders"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "alias repeating the relation name is used when referenced", + sql: "SELECT orders.id FROM orders orders", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before window clause has no alias", + sql: "SELECT SUM(amount) OVER w FROM orders WINDOW w AS (ORDER BY id)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before tablesample has no alias", + sql: "SELECT * FROM orders TABLESAMPLE BERNOULLI (10)", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unaliased relation before unpivot has no alias", + sql: "SELECT * FROM orders UNPIVOT (amount FOR month IN (jan, feb))", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias used as whole-row function argument", + sql: "SELECT row_to_json(o) AS payload FROM orders o", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit alias used as whole-row function argument", + sql: "SELECT row_to_json(o) AS payload FROM orders AS o", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias used as bare projection", + sql: "SELECT o FROM orders o", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit alias used as bare projection", + sql: "SELECT o FROM orders AS o", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "implicit alias used in whole-row comparison", + sql: "SELECT 1 AS present FROM orders o WHERE o IS NOT NULL", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "explicit alias used in whole-row comparison", + sql: "SELECT 1 AS present FROM orders AS o WHERE o IS NOT NULL", + rule: "SQC023", + expected_anchor: None, + expected_replacement: None, + }, + test_types::AdditionalLintRuleTestCase { + description: "unused implicit alias beside an unrelated qualified column is reported", + sql: "SELECT c.o FROM customers c CROSS JOIN orders o", + rule: "SQC023", + expected_anchor: Some("o"), + expected_replacement: Some(""), + }, + test_types::AdditionalLintRuleTestCase { + description: "unused explicit alias shadowed by an output alias is reported", + sql: "SELECT order_id AS o FROM orders AS o", + rule: "SQC023", + expected_anchor: Some("AS o"), + expected_replacement: Some(""), + }, + ]; + + for test_case in test_cases { + let diagnostics = + helpers::diagnostics_for_dialect(test_case.sql, "duckdb", &[test_case.rule])?; + assert_eq!( + diagnostics.len(), + usize::from(test_case.expected_anchor.is_some()), + "{}", + test_case.description + ); + let _ = test_case.expected_anchor.map(|expected_anchor| { + let diagnostic = &diagnostics[0]; + let start = diagnostic["start"].as_u64().unwrap_or_default() as usize; + let end = diagnostic["end"].as_u64().unwrap_or_default() as usize; + assert_eq!( + &test_case.sql[start..end], + expected_anchor, + "{}", + test_case.description + ); + assert_eq!( + diagnostic["fix"]["replacement"].as_str(), + test_case.expected_replacement, + "{}", + test_case.description + ); + }); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_lint/tests/test_values_formatter.rs b/crates/sqlclean/src/sql_lint/tests/test_values_formatter.rs new file mode 100644 index 0000000..6963aad --- /dev/null +++ b/crates/sqlclean/src/sql_lint/tests/test_values_formatter.rs @@ -0,0 +1,53 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::formatter::format_json; +use crate::sql_lint::tests::test_types; + +#[test] +fn given_mixed_values_relations_when_formatting_then_each_authored_form_is_preserved() +-> Result<(), String> { + let test_cases = [ + test_types::FormatTestCase { + description: "mixed values relations preserve parenthesized relation first", + sql: "SELECT * FROM (VALUES (1, 2)) AS first_values(a, b) UNION ALL SELECT * FROM VALUES (3, 4)", + expected_sql: "SELECT *\nFROM (VALUES (1, 2)) AS first_values(a, b)\nUNION ALL\nSELECT *\nFROM VALUES (3, 4)", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "mixed values relations preserve unparenthesized relation first", + sql: "SELECT * FROM VALUES (1, 2) UNION ALL SELECT * FROM (VALUES (3, 4)) AS second_values(a, b)", + expected_sql: "SELECT *\nFROM VALUES (1, 2)\nUNION ALL\nSELECT *\nFROM (VALUES (3, 4)) AS second_values(a, b)", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "mixed values relations inside joined subquery preserve authored forms", + sql: "SELECT base.id FROM items AS base JOIN (SELECT * FROM (VALUES (1)) AS first_values(id) UNION ALL SELECT * FROM VALUES (2)) AS mixed ON base.id = mixed.id", + expected_sql: "SELECT base.id\nFROM items AS base\nJOIN (\n SELECT *\n FROM (VALUES (1)) AS first_values(id)\n UNION ALL\n SELECT *\n FROM VALUES (2)\n) AS mixed\n ON base.id = mixed.id", + expected_changed: true, + }, + test_types::FormatTestCase { + description: "values text in string and comment is ignored", + sql: "-- VALUES and FROM VALUES are examples\nSELECT 'VALUES' AS label FROM (VALUES (1)) AS first_values(id) UNION ALL SELECT * FROM VALUES (2)", + expected_sql: "-- VALUES and FROM VALUES are examples\nSELECT 'VALUES' AS label\nFROM (VALUES (1)) AS first_values(id)\nUNION ALL\nSELECT *\nFROM VALUES (2)", + expected_changed: true, + }, + ]; + + for test_case in &test_cases { + let response = format_json( + &json!({"version": 1, "sql": test_case.sql, "dialect": "snowflake"}).to_string(), + )?; + let payload: Value = serde_json::from_str(&response).map_err(|error| error.to_string())?; + assert_eq!( + payload["sql"], test_case.expected_sql, + "{}", + test_case.description + ); + assert_eq!( + payload["changed"], test_case.expected_changed, + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/cte_columns.rs b/crates/sqlclean/src/sql_quality/_helpers/cte_columns.rs new file mode 100644 index 0000000..8f48d4b --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/cte_columns.rs @@ -0,0 +1,457 @@ +//! SQC042: output columns of non-final CTEs that no later step reads. + +use std::borrow::Cow; + +use polyglot_sql::expressions::{Cte, JoinKind, Select}; +use polyglot_sql::scope::walk_in_scope; +use polyglot_sql::{Expression, ExpressionWalk}; + +use crate::sql_lint::models::{LintDiagnostic, LintEdit}; +use crate::sql_quality::constants::{ + DISTINCT_UNUSED_REMEDIATION, GROUP_BY_ALL_UNUSED_REMEDIATION, GROUP_KEY_UNUSED_REMEDIATION, + INTERPOLATION_SENTINEL_PREFIXES, UNUSED_CTE_OUTPUT, +}; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::removal_safety::{expression_refusal, reads_whole_rows, select_refusal}; +use crate::sql_quality::syntax::{ + Projection, columns, cte_select_tokens, lower, projection_removal, projections, qualifier, + sources, +}; +use crate::sql_tokens::main::projection_spans::projection_spans; +use rustc_hash::{FxHashMap, FxHashSet}; + +const HARNESS_PREFIX: &str = "__"; +const COLUMNS_FUNCTION: &str = "COLUMNS"; +const LAYOUT_FIX_REFUSAL: &str = "The projection list could not be mapped to authored tokens"; + +/// What one later step reads from a CTE. +#[derive(Default)] +struct Usage { + names: FxHashSet, + everything: bool, + passes_to: Vec, + read: bool, +} + +impl Usage { + fn merged(mut self, other: Self) -> Self { + self.names.extend(other.names); + self.everything |= other.everything; + self.passes_to.extend(other.passes_to); + self.read |= other.read; + self + } +} + +/// One CTE whose unused outputs are being reported. +struct CteOutputs<'a> { + cte: &'a Cte, + name: &'a str, + usage: Option<&'a Usage>, +} + +pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Vec { + if request.fixture_body { + return Vec::new(); + } + let mut found: Vec = Vec::new(); + for statement in request.statements { + let Expression::Select(root) = statement else { + continue; + }; + let Some(with) = root.with.as_ref().filter(|with| !with.recursive) else { + continue; + }; + let mut terminal: Select = root.as_ref().clone(); + terminal.with = None; + let terminal: Expression = Expression::Select(Box::new(terminal)); + let names: Vec = with.ctes.iter().map(|cte| lower(&cte.alias.name)).collect(); + let used: FxHashMap = reads(&with.ctes, &names, &terminal); + let select_tokens: FxHashMap = cte_select_tokens(request.tokens); + for (index, cte) in with + .ctes + .iter() + .enumerate() + .take(with.ctes.len().saturating_sub(1)) + { + let name: &str = &names[index]; + if name.starts_with(HARNESS_PREFIX) || request.externally_referenced_ctes.contains(name) + { + continue; + } + found.extend(unused_outputs( + request, + &select_tokens, + CteOutputs { + cte, + name, + usage: used.get(name), + }, + )); + } + } + found +} + +/// Names each CTE's later readers use, following `SELECT *` pass-through to the reader. +fn reads(ctes: &[Cte], names: &[String], terminal: &Expression) -> FxHashMap { + let bodies: Vec<(Option, &Expression)> = ctes + .iter() + .map(|cte| (Some(lower(&cte.alias.name)), &cte.this)) + .chain(std::iter::once((None, terminal))) + .collect(); + let known: FxHashSet<&str> = names.iter().map(String::as_str).collect(); + let outputs: Vec>> = ctes.iter().map(output_names).collect(); + let mut readers: FxHashMap> = FxHashMap::default(); + for (index, (_, body)) in bodies.iter().enumerate() { + for name in read_relations(body) { + if known.contains(name.as_str()) { + readers.entry(name).or_default().push(index); + } + } + } + let mut usage: FxHashMap = FxHashMap::default(); + for (index, name) in names.iter().enumerate() { + let mut current: Usage = Usage::default(); + if index + 1 == names.len() { + current.everything = true; + } + for &reader in readers.get(name.as_str()).into_iter().flatten() { + if reader > index { + let (reader_name, body) = &bodies[reader]; + let target: Target<'_> = Target { + name, + outputs: outputs[index].as_ref(), + }; + current = current.merged(reader_usage(&target, reader_name.clone(), body)); + } + } + current.names.extend(self_reads(&ctes[index].this)); + usage.insert(name.clone(), current); + } + for index in (0..names.len()).rev() { + let passes: Vec = usage[&names[index]].passes_to.clone(); + for reader in passes { + let (everything, inherited): (bool, Vec) = + usage.get(&reader).map_or((true, Vec::new()), |next| { + (next.everything, next.names.iter().cloned().collect()) + }); + if let Some(current) = usage.get_mut(&names[index]) { + current.everything |= everything; + current.names.extend(inherited); + } + } + } + usage +} + +/// The output names of a CTE, or None when a star or unnamed projection hides them. +fn output_names(cte: &Cte) -> Option> { + let Expression::Select(select) = &cte.this else { + return None; + }; + projections(select) + .into_iter() + .map(|item| match item { + Projection::Named { name, .. } => Some(name), + _ => None, + }) + .collect() +} + +fn read_relations(body: &Expression) -> FxHashSet { + body.dfs() + .filter_map(|node| match node { + Expression::Table(table) => Some(lower(&table.name.name)), + _ => None, + }) + .collect() +} + +/// The CTE whose readers are being inspected. +struct Target<'a> { + name: &'a str, + outputs: Option<&'a FxHashSet>, +} + +fn reader_usage(target: &Target<'_>, reader: Option, body: &Expression) -> Usage { + let name: &str = target.name; + let mut usage: Usage = Usage::default(); + let mut aliases: FxHashSet = FxHashSet::default(); + let mut top_level_star: bool = false; + for node in body.dfs() { + let Expression::Select(select) = node else { + continue; + }; + let local: Vec = sources(select) + .into_iter() + .filter(|(_, relation, _)| relation.as_deref() == Some(name)) + .map(|(alias, _, _)| alias) + .collect(); + if local.is_empty() { + continue; + } + if select.joins.iter().any(|join| { + matches!( + join.kind, + JoinKind::Natural + | JoinKind::NaturalLeft + | JoinKind::NaturalRight + | JoinKind::NaturalFull + ) + }) { + usage.everything = true; + } + for join in &select.joins { + usage + .names + .extend(join.using.iter().map(|identifier| lower(&identifier.name))); + } + let top: bool = std::ptr::eq(node, body); + if reads_as_row(select, &local, target.outputs) { + usage.everything = true; + } + for projection in projections(select) { + if let Projection::Star(star) = projection + && star + .table + .as_ref() + .is_none_or(|table| local.contains(&lower(&table.name))) + { + if top + && reader.is_some() + && star.except.is_none() + && star.replace.is_none() + && !reads_whole_rows(select) + { + top_level_star = true; + } else { + usage.everything = true; + } + } + } + aliases.extend(local); + } + if aliases.is_empty() { + return usage; + } + usage.read = true; + usage.everything |= columns(body) + .into_iter() + .any(|column| is_interpolation(&column.name.name)); + if top_level_star && let Some(reader) = reader { + usage.passes_to.push(reader); + } + for column in body.dfs().filter_map(|node| match node { + Expression::Column(column) => Some(column), + _ => None, + }) { + if qualifier(column).is_none_or(|table| aliases.contains(&table)) { + usage.names.insert(lower(&column.name.name)); + } + } + usage +} + +/// Whether a reader consumes whole rows: a nested star, `COLUMNS(...)`, or the alias itself. +fn reads_as_row(select: &Select, local: &[String], outputs: Option<&FxHashSet>) -> bool { + for clause in clause_roots(select) { + for node in walk_in_scope(clause, false) { + if reads_row_at(select, node, local, outputs) { + return true; + } + } + } + false +} + +fn reads_row_at( + select: &Select, + node: &Expression, + local: &[String], + outputs: Option<&FxHashSet>, +) -> bool { + match node { + Expression::Star(star) => { + let projected: bool = select + .expressions + .iter() + .any(|item| std::ptr::eq(item, node)); + let table: Option = star.table.as_ref().map(|table| lower(&table.name)); + !projected && table.is_none_or(|table| local.contains(&table)) + } + Expression::BracedWildcard(_) => true, + Expression::Function(function) => function.name.eq_ignore_ascii_case(COLUMNS_FUNCTION), + Expression::Column(column) => { + let read: String = lower(&column.name.name); + let named_output: bool = outputs.is_some_and(|names| names.contains(&read)); + column.table.is_none() && local.contains(&read) && !named_output + } + _ => false, + } +} + +/// Root expressions of every clause of one select, without its FROM relations. +fn clause_roots(select: &Select) -> Vec<&Expression> { + let mut roots: Vec<&Expression> = select.expressions.iter().collect(); + roots.extend(select.where_clause.iter().map(|clause| &clause.this)); + roots.extend(select.having.iter().map(|clause| &clause.this)); + roots.extend(select.qualify.iter().map(|clause| &clause.this)); + if let Some(group) = &select.group_by { + roots.extend(group.expressions.iter()); + } + if let Some(order) = &select.order_by { + roots.extend(order.expressions.iter().map(|ordered| &ordered.this)); + } + roots.extend(select.joins.iter().filter_map(|join| join.on.as_ref())); + roots +} + +/// Lateral alias reuse: a later projection or a clause of the CTE reading an earlier output. +fn self_reads(body: &Expression) -> FxHashSet { + let mut reads: FxHashSet = FxHashSet::default(); + let Expression::Select(select) = body else { + return reads; + }; + let mut earlier: FxHashSet = FxHashSet::default(); + for item in projections(select) { + if let Projection::Named { name, expression } = item { + for read in unqualified_reads(expression) { + if earlier.contains(&read) && read != name { + reads.insert(read); + } + } + earlier.insert(name); + } + } + let mut clauses: Vec<&Expression> = Vec::new(); + clauses.extend(select.where_clause.iter().map(|clause| &clause.this)); + clauses.extend( + select + .group_by + .iter() + .flat_map(|group| group.expressions.iter()), + ); + clauses.extend(select.having.iter().map(|clause| &clause.this)); + clauses.extend(select.qualify.iter().map(|clause| &clause.this)); + if let Some(order) = &select.order_by { + clauses.extend(order.expressions.iter().map(|ordered| &ordered.this)); + } + for clause in clauses { + reads.extend( + unqualified_reads(clause) + .into_iter() + .filter(|read| earlier.contains(read)), + ); + } + reads +} + +fn unqualified_reads(expression: &Expression) -> Vec { + columns(expression) + .into_iter() + .filter(|column| qualifier(column).is_none()) + .map(|column| lower(&column.name.name)) + .collect() +} + +fn unused_outputs( + request: &QualityRequest<'_>, + select_tokens: &FxHashMap, + outputs: CteOutputs<'_>, +) -> Vec { + let CteOutputs { cte, name, usage } = outputs; + let Expression::Select(select) = &cte.this else { + return Vec::new(); + }; + let Some(usage) = usage.filter(|usage| !usage.everything && usage.read) else { + return Vec::new(); + }; + if !cte.columns.is_empty() { + return Vec::new(); + } + let output: Vec> = projections(select); + if !output + .iter() + .all(|item| matches!(item, Projection::Named { .. })) + { + return Vec::new(); + } + let spans: Vec<(usize, usize)> = select_tokens + .get(name) + .map(|&select_token| projection_spans(request.tokens, select_token)) + .unwrap_or_default(); + let grouped: FxHashSet = select + .group_by + .iter() + .flat_map(|group| group.expressions.iter()) + .flat_map(columns) + .map(|column| lower(&column.name.name)) + .collect(); + let select_refused: Option<&'static str> = select_refusal(select, output.len()); + let mut found: Vec = Vec::new(); + for (index, item) in output.iter().enumerate() { + let Projection::Named { + name: column, + expression, + } = item + else { + continue; + }; + if usage.names.contains(column) || is_interpolation(column) { + continue; + } + let grouping_key: bool = matches!(expression, Expression::Column(source) if grouped.contains(&lower(&source.name.name))); + let (start, end) = spans + .get(index) + .copied() + .filter(|_| spans.len() == output.len()) + .unwrap_or((0, 0)); + let refusal: Option<&'static str> = select_refused + .or_else(|| expression_refusal(expression)) + .or_else(|| (spans.len() != output.len()).then_some(LAYOUT_FIX_REFUSAL)); + let fix: Option = refusal + .is_none() + .then(|| projection_removal(&spans, index)) + .flatten() + .map(|(start, end)| LintEdit { + start, + end, + replacement: String::new(), + }); + found.push(LintDiagnostic { + code: UNUSED_CTE_OUTPUT.code, + message: Cow::Borrowed(UNUSED_CTE_OUTPUT.message), + remediation: Cow::Borrowed(if select.distinct { + DISTINCT_UNUSED_REMEDIATION + } else if select + .group_by + .as_ref() + .is_some_and(|group| group.all == Some(true)) + { + GROUP_BY_ALL_UNUSED_REMEDIATION + } else if grouping_key { + GROUP_KEY_UNUSED_REMEDIATION + } else { + UNUSED_CTE_OUTPUT.remediation + }), + start, + end, + fix_unavailable_reason: if fix.is_none() { + Some(refusal.unwrap_or(LAYOUT_FIX_REFUSAL)) + } else { + None + }, + fix, + }); + } + found +} + +/// Neutralized `@macro` / `@variable` interpolations can stand for any columns. +fn is_interpolation(name: &str) -> bool { + let name: String = lower(name); + INTERPOLATION_SENTINEL_PREFIXES + .iter() + .any(|prefix| name.starts_with(prefix)) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/declared_keys.rs b/crates/sqlclean/src/sql_quality/_helpers/declared_keys.rs new file mode 100644 index 0000000..02607a9 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/declared_keys.rs @@ -0,0 +1,50 @@ +//! Unique keys one relation declares through its incremental config and column audits. + +use crate::sql_quality::constants::{ + INCREMENTAL_MATERIALIZATION, KEY_PRESERVING_STRATEGIES, KEYED_DELETE_INSERT_STRATEGY, + MICROBATCH_MODE, NOT_NULL_AUDIT, UNIQUE_AUDIT, +}; +use crate::sql_quality::models::{DeclaredKeyColumn, DeclaredRelation}; + +/// `unique_key` only where the incremental strategy keeps one row per key. +pub(crate) fn trusted_unique_key(relation: &DeclaredRelation) -> Vec> { + let strategy: String = relation.incremental_strategy.to_lowercase(); + let trusted: bool = relation.materialized.to_lowercase() == INCREMENTAL_MATERIALIZATION + && relation.incremental_mode.to_lowercase() != MICROBATCH_MODE + && KEY_PRESERVING_STRATEGIES.contains(&strategy.as_str()) + && !(strategy == KEYED_DELETE_INSERT_STRATEGY && relation.has_cursor); + match &relation.unique_key { + Some(key) if trusted && !key.is_empty() => vec![key.clone()], + _ => Vec::new(), + } +} + +/// Columns with a `unique` audit that are also non-null by audit or `nullable false`. +pub(crate) fn non_null_unique_columns(relation: &DeclaredRelation) -> Vec { + let mut unique: Vec = Vec::new(); + let mut not_null: Vec = Vec::new(); + for column in &relation.columns { + if has_audit(column, UNIQUE_AUDIT) { + unique.push(column.name.clone()); + } + if has_audit(column, NOT_NULL_AUDIT) || column.not_nullable { + not_null.push(str::to_lowercase(&column.name)); + } + } + for (definition, argument) in &relation.audits { + let Some(argument) = argument else { + continue; + }; + if definition == NOT_NULL_AUDIT { + not_null.push(str::to_lowercase(argument)); + } else if definition == UNIQUE_AUDIT { + unique.push(argument.clone()); + } + } + unique.retain(|column| not_null.contains(&str::to_lowercase(column))); + unique +} + +fn has_audit(column: &DeclaredKeyColumn, name: &str) -> bool { + column.audits.iter().any(|audit| audit == name) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/json_parses.rs b/crates/sqlclean/src/sql_quality/_helpers/json_parses.rs new file mode 100644 index 0000000..ef7a3d3 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/json_parses.rs @@ -0,0 +1,201 @@ +//! SQC045: the same JSON payload column parsed more than once in one SELECT scope. + +use std::borrow::Cow; +use std::collections::HashMap; + +use polyglot_sql::expressions::{Column, Identifier}; +use polyglot_sql::scope::walk_in_scope; +use polyglot_sql::tokens::Token; +use polyglot_sql::{DialectType, Expression, ExpressionWalk}; + +use crate::sql_lint::models::LintDiagnostic; +use crate::sql_quality::constants::REPEATED_JSON_PARSE; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::syntax::{is_trivia, lower}; + +/// Significant tokens searched backwards from the column for the function name. +const MAX_FUNCTION_NAME_DISTANCE: usize = 32; +const PARSE_ONCE_FIX_REFUSAL: &str = + "Parsing the payload once requires naming a column in the CTE that reads it"; + +/// An identifier normalised the way the dialect resolves it. +type IdentifierKey = String; + +/// One JSON parse call whose argument resolves to a plain column. +struct ParseCall<'a> { + function: &'static str, + column: &'a Column, + column_start: usize, +} + +pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Vec { + let functions: &[&'static str] = parse_functions(request.dialect); + if functions.is_empty() { + return Vec::new(); + } + let mut found: Vec = Vec::new(); + for statement in request.statements { + for node in statement.dfs() { + if matches!(node, Expression::Select(_)) { + found.extend(scope_diagnostics( + request.tokens, + node, + functions, + request.dialect, + )); + } + } + } + found +} + +/// JSON parse functions that take one string argument, by dialect, in upper case. +fn parse_functions(dialect: DialectType) -> &'static [&'static str] { + match dialect { + DialectType::Snowflake | DialectType::Databricks => &["PARSE_JSON", "TRY_PARSE_JSON"], + DialectType::BigQuery => &["PARSE_JSON", "SAFE.PARSE_JSON"], + DialectType::DuckDB => &["JSON"], + _ => &[], + } +} + +fn scope_diagnostics( + tokens: &[Token], + select: &Expression, + functions: &[&'static str], + dialect: DialectType, +) -> Vec { + let quote: char = identifier_quote(dialect); + let mut calls: Vec> = walk_in_scope(select, false) + .filter_map(|node| parse_call(node, functions)) + .collect(); + calls.sort_by_key(|call| call.column_start); + let mut groups: HashMap< + (&'static str, Option, IdentifierKey), + Vec<&ParseCall<'_>>, + > = HashMap::new(); + for call in &calls { + let qualifier: Option = call + .column + .table + .as_ref() + .map(|table| identifier_key(table, dialect)); + groups + .entry(( + call.function, + qualifier, + identifier_key(&call.column.name, dialect), + )) + .or_default() + .push(call); + } + let mut found: Vec = Vec::new(); + for group in groups.values().filter(|group| group.len() > 1) { + let second: &ParseCall<'_> = group[1]; + let (start, end) = function_span(tokens, second); + found.push(LintDiagnostic { + code: REPEATED_JSON_PARSE.code, + message: Cow::Borrowed(REPEATED_JSON_PARSE.message), + remediation: Cow::Owned(remediation(second, group.len(), quote)), + start, + end, + fix: None, + fix_unavailable_reason: Some(PARSE_ONCE_FIX_REFUSAL), + }); + } + found +} + +/// Normalises a name the way the dialect resolves it (Snowflake folds unquoted names upward). +fn identifier_key(identifier: &Identifier, dialect: DialectType) -> IdentifierKey { + match dialect { + DialectType::Snowflake if identifier.quoted => identifier.name.clone(), + DialectType::Snowflake => identifier.name.to_uppercase(), + _ => lower(&identifier.name), + } +} + +fn parse_call<'a>(node: &'a Expression, functions: &[&'static str]) -> Option> { + let (name, argument): (&str, &Expression) = match node { + Expression::ParseJson(function) => ("PARSE_JSON", &function.this), + Expression::Function(function) if function.args.len() == 1 => { + (function.name.as_str(), &function.args[0]) + } + _ => return None, + }; + let function: &'static str = functions + .iter() + .copied() + .find(|candidate| candidate.eq_ignore_ascii_case(name))?; + let column: &Column = unwrap_column(argument)?; + let column_start: usize = column.span.or(column.name.span)?.start; + Some(ParseCall { + function, + column, + column_start, + }) +} + +/// The column under wrappers that keep the parsed value tied to that column. +fn unwrap_column(expression: &Expression) -> Option<&Column> { + let mut current: &Expression = expression; + loop { + current = match current { + Expression::Column(column) => return Some(column), + Expression::Paren(paren) => &paren.this, + Expression::Lower(function) | Expression::Upper(function) => &function.this, + Expression::Trim(trim) if trim.characters.is_none() => &trim.this, + Expression::Cast(cast) | Expression::TryCast(cast) | Expression::SafeCast(cast) => { + &cast.this + } + Expression::Coalesce(coalesce) if coalesce.expressions.len() == 1 => { + &coalesce.expressions[0] + } + _ => return None, + }; + } +} + +/// Span of the function-name token that opens `call`, or of its column when not found. +fn function_span(tokens: &[Token], call: &ParseCall<'_>) -> (usize, usize) { + let bare: &str = call.function.rsplit('.').next().unwrap_or(call.function); + let before: usize = tokens.partition_point(|token| token.span.start < call.column_start); + tokens[..before] + .iter() + .rev() + .filter(|token| !is_trivia(token)) + .take(MAX_FUNCTION_NAME_DISTANCE) + .find(|token| token.text.eq_ignore_ascii_case(bare)) + .map_or((call.column_start, call.column_start), |token| { + (token.span.start, token.span.end) + }) +} + +fn remediation(call: &ParseCall<'_>, count: usize, quote: char) -> String { + let name: String = written(&call.column.name, quote); + let column: String = call.column.table.as_ref().map_or_else( + || name.clone(), + |table| format!("{}.{name}", written(table, quote)), + ); + let function: &str = call.function; + format!( + "{function}({column}) is parsed {count} times in one SELECT. Parse it once in the CTE that reads it, for example `{function}({name}) AS payload`, and read every field from that column in a later step." + ) +} + +/// The identifier as authored, re-quoted with the dialect's identifier quote when it was quoted. +fn written(identifier: &Identifier, quote: char) -> String { + if identifier.quoted { + let escaped: String = identifier.name.replace(quote, &format!("{quote}{quote}")); + format!("{quote}{escaped}{quote}") + } else { + identifier.name.clone() + } +} + +fn identifier_quote(dialect: DialectType) -> char { + match dialect { + DialectType::BigQuery | DialectType::Databricks => '`', + _ => '"', + } +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/keys.rs b/crates/sqlclean/src/sql_quality/_helpers/keys.rs new file mode 100644 index 0000000..2e006e5 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/keys.rs @@ -0,0 +1,437 @@ +//! Unique-key propagation through one model's relations, for ranking determinism proofs. + +use std::collections::HashMap; + +use polyglot_sql::Expression; +use polyglot_sql::expressions::{JoinKind, Literal, Select}; + +use crate::sql_quality::constants::FIRST_ROW; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::syntax::{Projection, columns, lower, projections, qualifier, sources}; +use crate::sql_quality::types::{InputKey, Keys}; + +/// Keys known for relations by name: declared upstream keys and earlier CTE outputs. +pub(crate) struct KeyEnvironment { + pub(crate) declared: HashMap, + pub(crate) ctes: HashMap, + /// Per CTE, `(row number output, partition output columns)` for deduplication filters. + pub(crate) row_numbers: HashMap)>>, +} + +impl KeyEnvironment { + /// Output keys of a query expression, by output column name. + pub(crate) fn query_keys(&self, expression: &Expression) -> Keys { + match expression { + Expression::Select(select) => self.select_keys(select), + Expression::Subquery(subquery) => self.query_keys(&subquery.this), + Expression::Paren(paren) => self.query_keys(&paren.this), + Expression::Union(union) if !union.all => all_columns_key(&union.left), + Expression::Intersect(intersect) => self.query_keys(&intersect.left), + Expression::Except(except) => self.query_keys(&except.left), + _ => Vec::new(), + } + } + + fn relation_keys(&self, relation: &Expression) -> Keys { + match relation { + Expression::Table(table) => { + let name: String = lower(&table.name.name); + self.ctes + .get(&name) + .cloned() + .or_else(|| self.declared.get(&name).cloned()) + .unwrap_or_default() + } + Expression::Subquery(subquery) => self.query_keys(&subquery.this), + _ => Vec::new(), + } + } + + /// Keys of a select's joined and filtered input rows, before projection. + pub(crate) fn input_keys(&self, select: &Select) -> Vec { + if select + .from + .as_ref() + .is_none_or(|from| from.expressions.len() != 1) + { + return Vec::new(); + } + let found: Vec<(String, Option, &Expression)> = sources(select); + let Some((first_alias, _, first)) = found.first() else { + return Vec::new(); + }; + let mut keys: Vec = qualify(first_alias, self.relation_keys(first)); + keys.extend(qualify(first_alias, self.deduplicated_keys(select, first))); + for (join, (alias, _, relation)) in select.joins.iter().zip(found.iter().skip(1)) { + let right: Vec = qualify(alias, self.relation_keys(relation)); + let equated: Vec<(String, String)> = join_equalities(join, alias); + let left_columns: Vec<&str> = equated.iter().map(|(_, left)| left.as_str()).collect(); + let right_columns: Vec<&str> = + equated.iter().map(|(right, _)| right.as_str()).collect(); + let left_unique: bool = keys.iter().any(|key| key_within(key, &left_columns)); + let right_unique: bool = right.iter().any(|key| key_within(key, &right_columns)); + let mut joined: Vec = Vec::new(); + if right_unique + && !matches!( + join.kind, + JoinKind::Right | JoinKind::Full | JoinKind::Cross + ) + { + joined.extend(keys.iter().cloned()); + } + if left_unique && matches!(join.kind, JoinKind::Inner | JoinKind::Right) { + joined.extend(right.iter().cloned()); + } + keys = joined; + } + keys + } + + /// Input keys plus a `QUALIFY ROW_NUMBER() ... = n` partition, which holds only for output rows. + fn filtered_input_keys(&self, select: &Select) -> Vec { + let mut keys: Vec = self.input_keys(select); + if let Some(qualify_clause) = &select.qualify { + keys.extend(qualified_rank_keys(&qualify_clause.this, &sources(select))); + } + keys + } + + /// Partition keys of a `ROW_NUMBER()` output that this select filters to `= 1`. + fn deduplicated_keys(&self, select: &Select, relation: &Expression) -> Keys { + let Expression::Table(table) = relation else { + return Vec::new(); + }; + let Some(ranks) = self.row_numbers.get(&lower(&table.name.name)) else { + return Vec::new(); + }; + let filtered: Vec = select + .where_clause + .as_ref() + .map(|clause| first_row_filters(&clause.this)) + .unwrap_or_default(); + ranks + .iter() + .filter(|(rank, _)| filtered.contains(rank)) + .map(|(_, key)| key.clone()) + .collect() + } + + fn select_keys(&self, select: &Select) -> Keys { + let output: Vec> = projections(select); + let mut keys: Keys = Vec::new(); + if select.distinct + && output + .iter() + .all(|item| matches!(item, Projection::Named { .. })) + { + keys.push(output_names(&output)); + } + if let Some(group_by) = &select.group_by + && group_by.all.is_none() + && let Some(grain) = output_key_of(&group_by.expressions, &output) + { + keys.push(grain); + return keys; + } + let star: bool = output + .iter() + .any(|item| matches!(item, Projection::Star(_))); + for input in self.filtered_input_keys(select) { + if let Some(key) = project_key(&input, &output, star) { + keys.push(key); + } + } + keys + } +} + +fn qualify(alias: &str, keys: Keys) -> Vec { + keys.into_iter() + .map(|key| qualified_key(alias, key)) + .collect() +} + +fn qualified_key(alias: &str, key: Vec) -> InputKey { + key.into_iter() + .map(|column| (alias.to_owned(), column)) + .collect() +} + +fn key_within(key: &InputKey, columns: &[&str]) -> bool { + key.iter() + .all(|(_, column)| columns.contains(&column.as_str())) +} + +/// `(right column, left column)` pairs equated by a join's ON or USING condition. +fn join_equalities( + join: &polyglot_sql::expressions::Join, + right_alias: &str, +) -> Vec<(String, String)> { + let mut pairs: Vec<(String, String)> = join + .using + .iter() + .map(|identifier| (lower(&identifier.name), lower(&identifier.name))) + .collect(); + let mut pending: Vec<&Expression> = join.on.iter().collect(); + while let Some(expression) = pending.pop() { + match expression { + Expression::And(operation) => { + pending.push(&operation.left); + pending.push(&operation.right); + } + Expression::Paren(paren) => pending.push(&paren.this), + Expression::Eq(operation) => { + if let (Expression::Column(left), Expression::Column(right)) = + (&operation.left, &operation.right) + { + let (inner, outer) = if qualifier(left).as_deref() == Some(right_alias) { + (left, right) + } else { + (right, left) + }; + pairs.push((lower(&inner.name.name), lower(&outer.name.name))); + } + } + _ => {} + } + } + pairs +} + +fn qualified_rank_keys( + condition: &Expression, + found: &[(String, Option, &Expression)], +) -> Vec { + let Expression::Eq(operation) = condition else { + return Vec::new(); + }; + let window = match (&operation.left, &operation.right) { + (Expression::WindowFunction(window), Expression::Literal(_)) + | (Expression::Literal(_), Expression::WindowFunction(window)) => window, + _ => return Vec::new(), + }; + if !matches!(window.this, Expression::RowNumber(_)) || window.over.partition_by.is_empty() { + return Vec::new(); + } + let mut key: InputKey = InputKey::new(); + for expression in &window.over.partition_by { + let Expression::Column(column) = expression else { + return Vec::new(); + }; + let alias: String = qualifier(column) + .or_else(|| (found.len() == 1).then(|| found[0].0.clone())) + .unwrap_or_default(); + key.insert((alias, lower(&column.name.name))); + } + vec![key] +} + +fn output_names(output: &[Projection<'_>]) -> Vec { + output + .iter() + .filter_map(|item| match item { + Projection::Named { name, .. } => Some(name.clone()), + _ => None, + }) + .collect() +} + +fn output_key_of(expressions: &[Expression], output: &[Projection<'_>]) -> Option> { + let mut key: Vec = Vec::new(); + for expression in expressions { + if let Expression::Literal(literal) = expression + && let Literal::Number(text) = literal.as_ref() + { + let Ok(ordinal) = text.parse::() else { + return None; + }; + let Some(Projection::Named { name, .. }) = ordinal + .checked_sub(1) + .and_then(|position| output.get(position)) + else { + return None; + }; + key.push(name.clone()); + continue; + } + let Expression::Column(column) = expression else { + return None; + }; + let name: String = lower(&column.name.name); + let projected: Option = output.iter().find_map(|item| match item { + Projection::Named { + name: output_name, + expression: Expression::Column(source), + } if lower(&source.name.name) == name => Some(output_name.clone()), + Projection::Named { + name: output_name, .. + } if *output_name == name => Some(output_name.clone()), + _ => None, + }); + key.push(projected?); + } + Some(key) +} + +fn project_key(input: &InputKey, output: &[Projection<'_>], star: bool) -> Option> { + let mut key: Vec = Vec::new(); + for (alias, column) in input { + let projected: Option = output.iter().find_map(|item| match item { + Projection::Named { + name, + expression: Expression::Column(source), + } if lower(&source.name.name) == *column + && qualifier(source).is_none_or(|table| table == *alias) => + { + Some(name.clone()) + } + Projection::Star(star) + if star + .table + .as_ref() + .is_none_or(|table| lower(&table.name) == *alias) => + { + Some(column.clone()) + } + _ => None, + }); + match projected { + Some(name) => key.push(name), + None if star => key.push(column.clone()), + None => return None, + } + } + Some(key) +} + +fn all_columns_key(left: &Expression) -> Keys { + let Expression::Select(select) = left else { + return Vec::new(); + }; + let output: Vec> = projections(select); + if output + .iter() + .all(|item| matches!(item, Projection::Named { .. })) + { + vec![output_names(&output)] + } else { + Vec::new() + } +} + +/// Columns a ranking window partitions and orders by, generously resolved to input aliases. +pub(crate) fn window_columns( + partition_by: &[Expression], + order_by: &[&Expression], +) -> Vec<(Option, String)> { + partition_by + .iter() + .chain(order_by.iter().copied()) + .flat_map(columns) + .map(|column| (qualifier(column), lower(&column.name.name))) + .collect() +} + +/// Whether some input key is covered by the window's partition and order columns. +pub(crate) fn covers(keys: &[InputKey], window: &[(Option, String)]) -> bool { + keys.iter().any(|key| key_covered(key, window)) +} + +fn key_covered(key: &InputKey, window: &[(Option, String)]) -> bool { + key.iter() + .all(|(alias, column)| window_has(window, alias, column)) +} + +fn window_has(window: &[(Option, String)], alias: &str, column: &str) -> bool { + window.iter().any(|(qualifier, name)| { + name == column && qualifier.as_deref().is_none_or(|table| table == alias) + }) +} + +/// Keys of declared relations and of each top-level CTE, in definition order. +pub(crate) fn key_environment(request: &QualityRequest<'_>) -> KeyEnvironment { + let mut environment: KeyEnvironment = KeyEnvironment { + declared: lowered_keys(request.relation_keys), + ctes: HashMap::new(), + row_numbers: HashMap::new(), + }; + for statement in request.statements { + let Expression::Select(select) = statement else { + continue; + }; + for cte in select.with.iter().flat_map(|with| with.ctes.iter()) { + let keys = environment.query_keys(&cte.this); + environment.ctes.insert(lower(&cte.alias.name), keys); + if let Expression::Select(body) = &cte.this { + environment + .row_numbers + .insert(lower(&cte.alias.name), row_number_outputs(body)); + } + } + } + environment +} + +fn lowered_keys(keys: &HashMap) -> HashMap { + let mut lowered: HashMap = HashMap::new(); + for (name, relation_keys) in keys { + let mut converted: Keys = Vec::new(); + for key in relation_keys { + let mut columns: Vec = Vec::new(); + for column in key { + columns.push(lower(column)); + } + converted.push(columns); + } + lowered.insert(lower(name), converted); + } + lowered +} + +/// `(output name, partition output columns)` for each `ROW_NUMBER() OVER (PARTITION BY ...)` output. +fn row_number_outputs(select: &Select) -> Vec<(String, Vec)> { + let output: Vec> = projections(select); + let mut found: Vec<(String, Vec)> = Vec::new(); + for item in &output { + let Projection::Named { + name, + expression: Expression::WindowFunction(window), + } = item + else { + continue; + }; + if !matches!(window.this, Expression::RowNumber(_)) || window.over.partition_by.is_empty() { + continue; + } + if let Some(key) = output_key_of(&window.over.partition_by, &output) { + found.push((name.clone(), key)); + } + } + found +} + +/// Columns a filter pins to the first row, as in `WHERE rn = 1 AND ...`. +fn first_row_filters(condition: &Expression) -> Vec { + let mut found: Vec = Vec::new(); + let mut pending: Vec<&Expression> = vec![condition]; + while let Some(expression) = pending.pop() { + match expression { + Expression::And(operation) => { + pending.push(&operation.left); + pending.push(&operation.right); + } + Expression::Paren(paren) => pending.push(&paren.this), + Expression::Eq(operation) => { + if let (Expression::Column(column), Expression::Literal(literal)) + | (Expression::Literal(literal), Expression::Column(column)) = + (&operation.left, &operation.right) + && matches!(literal.as_ref(), Literal::Number(text) if text == FIRST_ROW) + { + found.push(lower(&column.name.name)); + } + } + _ => {} + } + } + found +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/literals.rs b/crates/sqlclean/src/sql_quality/_helpers/literals.rs new file mode 100644 index 0000000..25ca489 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/literals.rs @@ -0,0 +1,71 @@ +//! SQC044: string literals longer than the project limit. + +use std::borrow::Cow; + +use polyglot_sql::tokens::TokenType; + +use crate::sql_lint::models::LintDiagnostic; +use crate::sql_quality::constants::{ + LONG_LITERAL, LONG_LITERAL_GUIDANCE, MAX_LITERAL_LENGTH_KEY, RULES_THRESHOLDS_SECTION, +}; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::setting_help::{Setting, setting_help}; + +const CONSTANT_FIX_REFUSAL: &str = + "Moving the value to a constant requires choosing its name and placement"; + +pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Vec { + request + .tokens + .iter() + .filter(|token| is_string(token.token_type)) + .filter_map(|token| { + let length: usize = token.text.chars().count(); + (length > request.max_literal_length).then(|| LintDiagnostic { + code: LONG_LITERAL.code, + message: Cow::Borrowed(LONG_LITERAL.message), + remediation: Cow::Owned(remediation( + length, + request.max_literal_length, + request.config_file, + )), + start: token.span.start, + end: token.span.end, + fix: None, + fix_unavailable_reason: Some(CONSTANT_FIX_REFUSAL), + }) + }) + .collect() +} + +fn remediation(length: usize, limit: usize, config_file: &str) -> String { + format!( + "{LONG_LITERAL_GUIDANCE} {}", + setting_help(&Setting { + purpose: &format!( + "To allow literals of {length} characters (the current value is {limit})" + ), + config_file, + section: RULES_THRESHOLDS_SECTION, + key: MAX_LITERAL_LENGTH_KEY, + value: &length.to_string(), + }) + ) +} + +fn is_string(kind: TokenType) -> bool { + matches!( + kind, + TokenType::String + | TokenType::DollarString + | TokenType::EscapeString + | TokenType::RawString + | TokenType::NationalString + | TokenType::UnicodeString + | TokenType::ByteString + | TokenType::TripleSingleQuotedString + | TokenType::TripleDoubleQuotedString + | TokenType::HeredocString + | TokenType::HeredocStringAlternative + ) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/ranking.rs b/crates/sqlclean/src/sql_quality/_helpers/ranking.rs new file mode 100644 index 0000000..b1088df --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/ranking.rs @@ -0,0 +1,218 @@ +//! SQC043 ranking sort caps and SQC018 ranking determinism proofs. + +use std::borrow::Cow; +use std::collections::{HashMap, HashSet}; + +use polyglot_sql::expressions::{Over, Select, WindowFunction}; +use polyglot_sql::scope::walk_in_scope; +use polyglot_sql::tokens::Token; +use polyglot_sql::{Expression, ExpressionWalk}; + +use crate::sql_lint::models::LintDiagnostic; +use crate::sql_quality::constants::{ + MAX_RANKING_ORDER_BY_KEY, RANKING_SORT_CAP, RANKING_SORT_GUIDANCE, RULES_THRESHOLDS_SECTION, + TIE_SENSITIVE_FUNCTIONS, UNPROVEN_RANKING_MESSAGE, UNPROVEN_RANKING_REMEDIATION, + UNSTABLE_ROW_NUMBER_CODE, +}; +use crate::sql_quality::keys::{KeyEnvironment, covers, window_columns}; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::setting_help::{Setting, setting_help}; +use crate::sql_quality::syntax::{columns, lower}; +use crate::sql_quality::types::InputKey; + +const PRIORITY_FIX_REFUSAL: &str = + "Choosing a priority column and tie-breaker requires author intent"; +const KEY_FIX_REFUSAL: &str = "Declaring a key or adding a tie-breaker requires author intent"; + +struct RankingWindow<'a> { + function: &'static str, + window: &'a WindowFunction, + select: &'a Select, +} + +pub(crate) fn diagnostics( + request: &QualityRequest<'_>, + environment: &KeyEnvironment, +) -> Vec { + let cap: bool = request.enabled.contains(RANKING_SORT_CAP.code); + let proof: bool = request.enabled.contains(UNSTABLE_ROW_NUMBER_CODE); + let mut used: HashSet = HashSet::new(); + let mut function_tokens: HashMap> = HashMap::new(); + for (index, token) in request.tokens.iter().enumerate() { + if TIE_SENSITIVE_FUNCTIONS + .iter() + .chain(["RANK", "DENSE_RANK"].iter()) + .any(|name| token.text.eq_ignore_ascii_case(name)) + { + function_tokens + .entry(token.text.to_ascii_uppercase()) + .or_default() + .push(index); + } + } + let mut found: Vec = Vec::new(); + for ranking in ranking_windows(request.statements) { + let over: Option<&Over> = effective_over(&ranking.window.over, ranking.select); + let order: Vec<&Expression> = over.map_or_else(Vec::new, |over| { + over.order_by.iter().map(|ordered| &ordered.this).collect() + }); + let partition: &[Expression] = over.map_or(&[], |over| over.partition_by.as_slice()); + let exceeds: bool = cap && order.len() > request.max_ranking_order_by; + let unproven: bool = proof + && !order.is_empty() + && TIE_SENSITIVE_FUNCTIONS.contains(&ranking.function) + && !proven(environment, ranking.select, partition, &order); + if !exceeds && !unproven { + continue; + } + let candidates: &[usize] = function_tokens + .get(ranking.function) + .map_or(&[], Vec::as_slice); + let token: Option = function_token( + request.tokens, + candidates, + window_anchor(partition, &order), + &used, + ); + let (start, end) = token.map_or((0, 0), |index| { + ( + request.tokens[index].span.start, + request.tokens[index].span.end, + ) + }); + used.extend(token); + if exceeds { + found.push(LintDiagnostic { + code: RANKING_SORT_CAP.code, + message: Cow::Borrowed(RANKING_SORT_CAP.message), + remediation: Cow::Owned(sort_cap_remediation( + order.len(), + request.max_ranking_order_by, + request.config_file, + )), + start, + end, + fix: None, + fix_unavailable_reason: Some(PRIORITY_FIX_REFUSAL), + }); + } + if unproven { + found.push(LintDiagnostic { + code: UNSTABLE_ROW_NUMBER_CODE, + message: Cow::Borrowed(UNPROVEN_RANKING_MESSAGE), + remediation: Cow::Borrowed(UNPROVEN_RANKING_REMEDIATION), + start, + end, + fix: None, + fix_unavailable_reason: Some(KEY_FIX_REFUSAL), + }); + } + } + found +} + +fn sort_cap_remediation(sort_keys: usize, limit: usize, config_file: &str) -> String { + format!( + "{RANKING_SORT_GUIDANCE} {}", + setting_help(&Setting { + purpose: &format!("To allow {sort_keys} sort keys (the current value is {limit})"), + config_file, + section: RULES_THRESHOLDS_SECTION, + key: MAX_RANKING_ORDER_BY_KEY, + value: &sort_keys.to_string(), + }) + ) +} + +fn proven( + environment: &KeyEnvironment, + select: &Select, + partition: &[Expression], + order: &[&Expression], +) -> bool { + let keys: Vec = environment.input_keys(select); + covers(&keys, &window_columns(partition, order)) +} + +fn ranking_windows(statements: &[Expression]) -> Vec> { + let mut found: Vec> = Vec::new(); + for statement in statements { + for node in statement.dfs() { + let Expression::Select(select) = node else { + continue; + }; + for local in walk_in_scope(node, false) { + let Expression::WindowFunction(window) = local else { + continue; + }; + let function: &'static str = match &window.this { + Expression::RowNumber(_) => "ROW_NUMBER", + Expression::Rank(_) => "RANK", + Expression::DenseRank(_) => "DENSE_RANK", + Expression::FirstValue(_) => "FIRST_VALUE", + Expression::LastValue(_) => "LAST_VALUE", + _ => continue, + }; + found.push(RankingWindow { + function, + window, + select, + }); + } + } + } + found +} + +fn effective_over<'a>(over: &'a Over, select: &'a Select) -> Option<&'a Over> { + let mut current: &Over = over; + let mut seen: HashSet = HashSet::new(); + loop { + if !current.order_by.is_empty() || current.window_name.is_none() { + return Some(current); + } + let name: String = lower(¤t.window_name.as_ref()?.name); + if !seen.insert(name.clone()) { + return None; + } + current = &select + .windows + .iter() + .flatten() + .find(|window| lower(&window.name.name) == name)? + .spec; + } +} + +/// Start offset of the first located column a window partitions or orders by. +fn window_anchor(partition: &[Expression], order: &[&Expression]) -> Option { + let mut expressions: Vec<&Expression> = partition.iter().collect(); + expressions.extend(order.iter().copied()); + for expression in expressions { + for column in columns(expression) { + if let Some(span) = column.span { + return Some(span.start); + } + } + } + None +} + +/// The function-name token before the anchor, or the first token not reported yet. +fn function_token( + tokens: &[Token], + candidates: &[usize], + anchor: Option, + used: &HashSet, +) -> Option { + if let Some(anchor) = anchor { + let before: usize = candidates.partition_point(|&index| tokens[index].span.start < anchor); + if let Some(position) = before.checked_sub(1) { + return Some(candidates[position]); + } + } + candidates + .iter() + .copied() + .find(|index| !used.contains(index)) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/removal_safety.rs b/crates/sqlclean/src/sql_quality/_helpers/removal_safety.rs new file mode 100644 index 0000000..92f6025 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/removal_safety.rs @@ -0,0 +1,102 @@ +//! SQC042 allow-list: when removing an unused CTE output provably keeps every row. + +use polyglot_sql::expressions::Select; +use polyglot_sql::traversal::contains_aggregate; +use polyglot_sql::{Expression, ExpressionWalk}; + +use crate::sql_quality::constants::{ + DETERMINISTIC_SCALAR_FUNCTIONS, MIN_REMOVABLE_OUTPUTS, ROW_NEUTRAL_EXPRESSIONS, +}; + +const ORDER_BY_ALL: &str = "all"; +const DISTINCT_REFUSAL: &str = "SELECT DISTINCT depends on every output column"; +const DISTINCT_ON_REFUSAL: &str = "SELECT DISTINCT ON keeps one row per key chosen by the select"; +const GROUP_BY_ALL_REFUSAL: &str = "GROUP BY ALL groups by every non-aggregate output column"; +const ORDER_BY_ALL_REFUSAL: &str = "ORDER BY ALL sorts by every output column"; +const UNGROUPED_AGGREGATE_REFUSAL: &str = + "An aggregate select without GROUP BY depends on its whole projection"; +const POSITIONAL_REFUSAL: &str = "Positional GROUP BY or ORDER BY would shift"; +const ONLY_OUTPUT_REFUSAL: &str = "Removing the only output column leaves an empty select"; +const EXPRESSION_REFUSAL: &str = "Only column references and deterministic scalar expressions are removed automatically; this expression may change the rows the CTE returns"; + +/// Why removing any output of `select` could change its rows, if it could. +pub(super) fn select_refusal(select: &Select, outputs: usize) -> Option<&'static str> { + if select.distinct { + Some(DISTINCT_REFUSAL) + } else if select.distinct_on.is_some() { + Some(DISTINCT_ON_REFUSAL) + } else if select + .group_by + .as_ref() + .is_some_and(|group| group.all == Some(true)) + { + Some(GROUP_BY_ALL_REFUSAL) + } else if orders_by_all(select) { + Some(ORDER_BY_ALL_REFUSAL) + } else if select.group_by.is_none() + && (select.having.is_some() || select.expressions.iter().any(contains_aggregate)) + { + Some(UNGROUPED_AGGREGATE_REFUSAL) + } else if positional_clauses(select) { + Some(POSITIONAL_REFUSAL) + } else if outputs < MIN_REMOVABLE_OUTPUTS { + Some(ONLY_OUTPUT_REFUSAL) + } else { + None + } +} + +/// Why removing this one projection could change the rows, if it could. +pub(super) fn expression_refusal(expression: &Expression) -> Option<&'static str> { + (!row_neutral(expression)).then_some(EXPRESSION_REFUSAL) +} + +/// Whether a star select depends on every input column or on their positions. +pub(super) fn reads_whole_rows(select: &Select) -> bool { + select.distinct + || select.distinct_on.is_some() + || select.group_by.is_some() + || orders_by_all(select) + || positional_clauses(select) +} + +/// A column reference or deterministic scalar expression (no aggregate, window, SRF or subquery). +fn row_neutral(expression: &Expression) -> bool { + expression.dfs().all(|node| match node { + Expression::Function(function) => { + let name: String = function.name.to_ascii_uppercase(); + !function.distinct && DETERMINISTIC_SCALAR_FUNCTIONS.contains(&name.as_str()) + } + other => ROW_NEUTRAL_EXPRESSIONS.contains(&other.variant_name()), + }) +} + +fn orders_by_all(select: &Select) -> bool { + select + .order_by + .iter() + .flat_map(|order| order.expressions.iter()) + .any(|ordered| match &ordered.this { + Expression::Column(column) => { + column.table.is_none() + && !column.name.quoted + && column.name.name.eq_ignore_ascii_case(ORDER_BY_ALL) + } + Expression::Var(var) => var.this.eq_ignore_ascii_case(ORDER_BY_ALL), + _ => false, + }) +} + +fn positional_clauses(select: &Select) -> bool { + let numeric = |expression: &Expression| matches!(expression, Expression::Literal(_)); + select + .group_by + .iter() + .flat_map(|group| group.expressions.iter()) + .any(numeric) + || select + .order_by + .iter() + .flat_map(|order| order.expressions.iter()) + .any(|ordered| numeric(&ordered.this)) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/setting_help.rs b/crates/sqlclean/src/sql_quality/_helpers/setting_help.rs new file mode 100644 index 0000000..a456ab7 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/setting_help.rs @@ -0,0 +1,26 @@ +//! Remediation text that shows the exact configuration setting to write. + +use crate::constants::SETTING_SNIPPET_INDENT; + +/// One setting a remediation tells the user to write. +pub(crate) struct Setting<'a> { + pub(crate) purpose: &'a str, + pub(crate) config_file: &'a str, + pub(crate) section: &'a str, + pub(crate) key: &'a str, + pub(crate) value: &'a str, +} + +/// `, set this in :` followed by the section and `key = value`. +pub(crate) fn setting_help(setting: &Setting<'_>) -> String { + let Setting { + purpose, + config_file, + section, + key, + value, + } = setting; + format!( + "{purpose}, set this in {config_file}:\n{SETTING_SNIPPET_INDENT}[{section}]\n{SETTING_SNIPPET_INDENT}{key} = {value}" + ) +} diff --git a/crates/sqlclean/src/sql_quality/_helpers/syntax.rs b/crates/sqlclean/src/sql_quality/_helpers/syntax.rs new file mode 100644 index 0000000..7f79990 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/_helpers/syntax.rs @@ -0,0 +1,124 @@ +//! Shared syntax helpers for SQL quality Rules. + +use polyglot_sql::expressions::{Column, Select, Star}; +use polyglot_sql::tokens::{Token, TokenType}; +use polyglot_sql::{Expression, ExpressionWalk}; + +use crate::sql_quality::constants::MIN_REMOVABLE_OUTPUTS; +use rustc_hash::FxHashMap; + +/// One projection of a select list. +pub(super) enum Projection<'a> { + Named { + name: String, + expression: &'a Expression, + }, + Star(&'a Star), + Unnamed, +} + +pub(super) fn lower(name: &str) -> String { + name.to_ascii_lowercase() +} + +pub(super) fn projections(select: &Select) -> Vec> { + select + .expressions + .iter() + .map(|expression| match expression { + Expression::Alias(alias) => Projection::Named { + name: lower(&alias.alias.name), + expression: &alias.this, + }, + Expression::Column(column) => Projection::Named { + name: lower(&column.name.name), + expression, + }, + Expression::Star(star) => Projection::Star(star), + _ => Projection::Unnamed, + }) + .collect() +} + +pub(super) fn columns(expression: &Expression) -> Vec<&Column> { + expression + .dfs() + .filter_map(|node| match node { + Expression::Column(column) => Some(column.as_ref()), + _ => None, + }) + .collect() +} + +pub(super) fn qualifier(column: &Column) -> Option { + column.table.as_ref().map(|table| lower(&table.name)) +} + +/// Relations read directly by one select: `(alias, relation name, expression)`. +pub(super) fn sources(select: &Select) -> Vec<(String, Option, &Expression)> { + let mut found: Vec<(String, Option, &Expression)> = Vec::new(); + let from = select.from.iter().flat_map(|from| from.expressions.iter()); + for expression in from.chain(select.joins.iter().map(|join| &join.this)) { + match expression { + Expression::Table(table) => { + let name: String = lower(&table.name.name); + let alias: String = table + .alias + .as_ref() + .map_or_else(|| name.clone(), |alias| lower(&alias.name)); + found.push((alias, Some(name), expression)); + } + Expression::Subquery(subquery) => { + let alias: String = subquery + .alias + .as_ref() + .map_or_else(String::new, |alias| lower(&alias.name)); + found.push((alias, None, expression)); + } + _ => found.push((String::new(), None, expression)), + } + } + found +} + +/// Token index of the SELECT opening each `name AS (SELECT` CTE body, by lower-cased name. +pub(super) fn cte_select_tokens(tokens: &[Token]) -> FxHashMap { + let significant: Vec = (0..tokens.len()) + .filter(|&index| !is_trivia(&tokens[index])) + .collect(); + let mut found: FxHashMap = FxHashMap::default(); + for window in significant.windows(4) { + let [name_index, as_index, paren_index, select_index] = window else { + continue; + }; + if tokens[*as_index].token_type == TokenType::As + && tokens[*paren_index].token_type == TokenType::LParen + && tokens[*select_index].token_type == TokenType::Select + { + found + .entry(lower(&tokens[*name_index].text)) + .or_insert(*select_index); + } + } + found +} + +/// The comma-inclusive deletion span that removes projection `index` from `spans`. +pub(super) fn projection_removal(spans: &[(usize, usize)], index: usize) -> Option<(usize, usize)> { + if spans.len() < MIN_REMOVABLE_OUTPUTS { + return None; + } + let (start, end) = spans[index]; + if index + 1 < spans.len() { + Some((start, spans[index + 1].0)) + } else { + Some((spans[index - 1].1, end)) + } +} + +pub(super) fn is_trivia(token: &Token) -> bool { + matches!( + token.token_type, + TokenType::Space | TokenType::Break | TokenType::LineComment | TokenType::BlockComment + ) +} diff --git a/crates/sqlclean/src/sql_quality/constants.rs b/crates/sqlclean/src/sql_quality/constants.rs new file mode 100644 index 0000000..25ae410 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/constants.rs @@ -0,0 +1,202 @@ +use crate::sql_lint::models::LintRuleMetadata; + +pub(crate) const UNUSED_CTE_OUTPUT: LintRuleMetadata = LintRuleMetadata { + code: "SQC042", + message: "CTE output column is never read later in the query", + remediation: "Remove the column from the CTE output, or read it in a later CTE or the final query.", +}; +pub(crate) const RANKING_SORT_CAP: LintRuleMetadata = LintRuleMetadata { + code: "SQC043", + message: "Ranking window sorts by more expressions than max_ranking_order_by", + remediation: "Order by the one or two columns that decide which row wins, then a unique row key as the final tie-breaker.", +}; +pub(crate) const LONG_LITERAL: LintRuleMetadata = LintRuleMetadata { + code: "SQC044", + message: "String literal is longer than max_literal_length", + remediation: "Shorten the literal, or move the value into a reference table or a query parameter.", +}; +pub(crate) const REPEATED_JSON_PARSE: LintRuleMetadata = LintRuleMetadata { + code: "SQC045", + message: "JSON payload is parsed more than once in one SELECT", + remediation: "Parse the payload once in the CTE that reads it, for example `TRY_PARSE_JSON(value) AS payload`, and read every field from that column in a later step.", +}; +pub(crate) const UNSTABLE_ROW_NUMBER_CODE: &str = "SQC018"; +pub(crate) const UNPROVEN_RANKING_MESSAGE: &str = + "Ranking window order is not proven unique within its partition"; +pub(crate) const UNPROVEN_RANKING_REMEDIATION: &str = "PARTITION BY and ORDER BY must together cover a unique key of the ranked rows. End the ORDER BY with a unique, non-null tie-breaker column. If the rows have no unique column, build one upstream from the columns that identify a row (for example `HASH(source_partition, source_offset, line_index) AS row_key`) and order by it last."; +/// SQC043 guidance before the `max_ranking_order_by` setting snippet. +pub(crate) const RANKING_SORT_GUIDANCE: &str = "Order by the one or two columns that decide which row wins, then a unique row key as the final tie-breaker. If the rows have no unique column, build one upstream from the columns that identify a row (for example `HASH(source_partition, source_offset, line_index) AS row_key`), so SQC018 can prove the order is deterministic. If the order genuinely needs more keys, raise `max_ranking_order_by`."; +/// SQC044 guidance before the `max_literal_length` setting snippet. +pub(crate) const LONG_LITERAL_GUIDANCE: &str = "Shorten the literal, or move the value into a reference table or a query parameter so it is defined once."; +pub(crate) const RULES_THRESHOLDS_SECTION: &str = "rules.thresholds"; +pub(crate) const MAX_RANKING_ORDER_BY_KEY: &str = "max_ranking_order_by"; +pub(crate) const MAX_LITERAL_LENGTH_KEY: &str = "max_literal_length"; +pub(crate) const DISTINCT_UNUSED_REMEDIATION: &str = "This CTE uses SELECT DISTINCT, so the column still decides which rows are kept. Make the deduplication independent of the column, or restructure the CTE so it outputs only what later steps read."; +pub(crate) const GROUP_KEY_UNUSED_REMEDIATION: &str = "Drop the column from the CTE output but keep it in GROUP BY; the grouping grain stays the same."; +pub(crate) const GROUP_BY_ALL_UNUSED_REMEDIATION: &str = "This CTE uses GROUP BY ALL, so the column is part of the grouping grain. List the grouping columns explicitly and keep this one in GROUP BY, then drop it from the output."; +/// Expression kinds that compute one value per input row without changing the row set. +pub(crate) const ROW_NEUTRAL_EXPRESSIONS: &[&str] = &[ + "literal", + "boolean", + "null", + "identifier", + "column", + "paren", + "dot", + "subscript", + "data_type", + "interval", + "cast", + "try_cast", + "safe_cast", + "collation", + "case", + "and", + "or", + "not", + "neg", + "add", + "sub", + "mul", + "div", + "mod", + "eq", + "neq", + "lt", + "lte", + "gt", + "gte", + "like", + "i_like", + "is", + "is_null", + "is_true", + "is_false", + "in", + "between", + "null_safe_eq", + "bitwise_and", + "bitwise_or", + "bitwise_xor", + "concat", + "concat_ws", + "substring", + "upper", + "lower", + "length", + "trim", + "replace", + "left", + "right", + "lpad", + "rpad", + "split", + "split_part", + "regexp_replace", + "initcap", + "position", + "contains", + "starts_with", + "ends_with", + "abs", + "round", + "floor", + "ceil", + "greatest", + "least", + "coalesce", + "null_if", + "if_func", + "if_null", + "nvl", + "nvl2", + "decode", + "date_add", + "date_diff", + "date_trunc", + "timestamp_trunc", + "extract", + "to_date", + "to_timestamp", + "to_char", + "to_number", + "date", + "year", + "month", + "last_day", + "struct_extract", +]; +/// Generic function names known to be deterministic scalar functions in supported dialects. +pub(crate) const DETERMINISTIC_SCALAR_FUNCTIONS: &[&str] = &[ + "ABS", + "ARRAY_SIZE", + "CEIL", + "CHARINDEX", + "COALESCE", + "CONCAT", + "CONCAT_WS", + "DATEADD", + "DATEDIFF", + "DATE_ADD", + "DATE_DIFF", + "DATE_PART", + "DATE_TRUNC", + "DAY", + "DIV0", + "FLOOR", + "GREATEST", + "HOUR", + "IFF", + "IFNULL", + "LEAST", + "LEFT", + "LENGTH", + "LOWER", + "LPAD", + "LTRIM", + "MD5", + "MOD", + "MONTH", + "NULLIF", + "NVL", + "NVL2", + "POWER", + "REGEXP_REPLACE", + "REPLACE", + "RIGHT", + "ROUND", + "RPAD", + "RTRIM", + "SPLIT_PART", + "SQRT", + "STRPOS", + "SUBSTR", + "SUBSTRING", + "TO_CHAR", + "TO_DATE", + "TO_VARCHAR", + "TRIM", + "TRUNC", + "UPPER", + "YEAR", + "ZEROIFNULL", +]; +pub(crate) const DEFAULT_MAX_LITERAL_LENGTH: usize = 100; +pub(crate) const DEFAULT_MAX_RANKING_ORDER_BY: usize = 5; +pub(crate) const TIE_SENSITIVE_FUNCTIONS: [&str; 3] = ["ROW_NUMBER", "FIRST_VALUE", "LAST_VALUE"]; +pub(crate) const MIN_REMOVABLE_OUTPUTS: usize = 2; +pub(crate) const INTERPOLATION_SENTINEL_PREFIXES: [&str; 3] = [ + "__sqb_lint_", + "__sqlbuild_audit_parameter_", + "__sqlbuild_context_parameter_", +]; +pub(crate) const FIRST_ROW: &str = "1"; + +pub(crate) const UNIQUE_AUDIT: &str = "unique"; +pub(crate) const NOT_NULL_AUDIT: &str = "not_null"; +pub(crate) const INCREMENTAL_MATERIALIZATION: &str = "incremental"; +pub(crate) const MICROBATCH_MODE: &str = "microbatch"; +pub(crate) const KEYED_DELETE_INSERT_STRATEGY: &str = "delete_insert"; +pub(crate) const KEY_PRESERVING_STRATEGIES: [&str; 2] = ["merge", KEYED_DELETE_INSERT_STRATEGY]; +pub(crate) const RELATION_KIND_REF: &str = "ref"; +pub(crate) const RELATION_KIND_SEED: &str = "seed"; diff --git a/crates/sqlclean/src/sql_quality/main/declared_relation_keys.rs b/crates/sqlclean/src/sql_quality/main/declared_relation_keys.rs new file mode 100644 index 0000000..f999212 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/main/declared_relation_keys.rs @@ -0,0 +1,38 @@ +use std::collections::BTreeMap; + +use crate::sql_quality::constants::{RELATION_KIND_REF, RELATION_KIND_SEED}; +use crate::sql_quality::declared_keys::{non_null_unique_columns, trusted_unique_key}; +use crate::sql_quality::models::DeclaredRelation; + +/// Keys proving one row per key by `kind:name`; seeds are also refs, one-row relations key `[]`. +pub fn declared_relation_keys( + relations: &[DeclaredRelation], +) -> BTreeMap>> { + let mut keys: BTreeMap>> = BTreeMap::new(); + for relation in relations { + let mut found: Vec> = trusted_unique_key(relation); + for column in non_null_unique_columns(relation) { + found.push(vec![column]); + } + if relation.at_most_one_row { + found.push(Vec::new()); + } + if found.is_empty() { + continue; + } + let mut kinds: Vec<&str> = vec![relation.kind.as_str()]; + if relation.kind == RELATION_KIND_SEED { + kinds.push(RELATION_KIND_REF); + } + for kind in kinds { + let entry: &mut Vec> = + keys.entry(format!("{kind}:{}", relation.name)).or_default(); + for key in &found { + if !entry.contains(key) { + entry.push(key.clone()); + } + } + } + } + keys +} diff --git a/crates/sqlclean/src/sql_quality/main/diagnostics.rs b/crates/sqlclean/src/sql_quality/main/diagnostics.rs new file mode 100644 index 0000000..76848cc --- /dev/null +++ b/crates/sqlclean/src/sql_quality/main/diagnostics.rs @@ -0,0 +1,32 @@ +use crate::sql_lint::models::LintDiagnostic; +use crate::sql_quality::constants::{ + LONG_LITERAL, RANKING_SORT_CAP, REPEATED_JSON_PARSE, UNSTABLE_ROW_NUMBER_CODE, + UNUSED_CTE_OUTPUT, +}; +use crate::sql_quality::keys::{KeyEnvironment, key_environment}; +use crate::sql_quality::models::QualityRequest; +use crate::sql_quality::{cte_columns, json_parses, literals, ranking}; + +/// Run every enabled SQL quality Rule over one parsed body. +pub(crate) fn diagnostics(request: &QualityRequest<'_>) -> Result, String> { + if request.max_literal_length == 0 || request.max_ranking_order_by == 0 { + return Err("max_literal_length and max_ranking_order_by must be positive".to_owned()); + } + let mut found: Vec = Vec::new(); + if request.enabled.contains(LONG_LITERAL.code) { + found.extend(literals::diagnostics(request)); + } + if request.enabled.contains(REPEATED_JSON_PARSE.code) { + found.extend(json_parses::diagnostics(request)); + } + if request.enabled.contains(UNUSED_CTE_OUTPUT.code) { + found.extend(cte_columns::diagnostics(request)); + } + if request.enabled.contains(RANKING_SORT_CAP.code) + || request.enabled.contains(UNSTABLE_ROW_NUMBER_CODE) + { + let environment: KeyEnvironment = key_environment(request); + found.extend(ranking::diagnostics(request, &environment)); + } + Ok(found) +} diff --git a/crates/sqlclean/src/sql_quality/main/mod.rs b/crates/sqlclean/src/sql_quality/main/mod.rs new file mode 100644 index 0000000..f8c958d --- /dev/null +++ b/crates/sqlclean/src/sql_quality/main/mod.rs @@ -0,0 +1,2 @@ +pub mod declared_relation_keys; +pub mod diagnostics; diff --git a/crates/sqlclean/src/sql_quality/mod.rs b/crates/sqlclean/src/sql_quality/mod.rs new file mode 100644 index 0000000..b47f562 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/mod.rs @@ -0,0 +1,27 @@ +//! Enforceable SQL quality Rules with project-wide limits and ranking determinism proofs. + +pub mod constants; +#[path = "_helpers/cte_columns.rs"] +mod cte_columns; +#[path = "_helpers/declared_keys.rs"] +mod declared_keys; +#[path = "_helpers/json_parses.rs"] +mod json_parses; +#[path = "_helpers/keys.rs"] +mod keys; +#[path = "_helpers/literals.rs"] +mod literals; +pub mod main; +pub mod models; +#[path = "_helpers/ranking.rs"] +mod ranking; +#[path = "_helpers/removal_safety.rs"] +mod removal_safety; +#[path = "_helpers/setting_help.rs"] +mod setting_help; +#[path = "_helpers/syntax.rs"] +mod syntax; +pub mod types; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean/src/sql_quality/models.rs b/crates/sqlclean/src/sql_quality/models.rs new file mode 100644 index 0000000..577d6a0 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/models.rs @@ -0,0 +1,47 @@ +use std::collections::{HashMap, HashSet}; + +use polyglot_sql::tokens::Token; +use polyglot_sql::{DialectType, Expression}; + +/// Inputs for one body's quality Rules, borrowed from the native lint request. +#[derive(Debug)] +pub(crate) struct QualityRequest<'a> { + pub(crate) statements: &'a [Expression], + pub(crate) tokens: &'a [Token], + pub(crate) dialect: DialectType, + pub(crate) enabled: &'a HashSet, + pub(crate) max_literal_length: usize, + pub(crate) max_ranking_order_by: usize, + pub(crate) relation_keys: &'a HashMap>>, + pub(crate) externally_referenced_ctes: &'a HashSet, + pub(crate) fixture_body: bool, + /// The configuration file remediation text tells the user to edit. + pub(crate) config_file: &'a str, +} + +/// One project relation's declared key facts: its incremental config and audited columns. +#[derive(Debug, Clone, Default)] +pub struct DeclaredRelation { + /// `ref`, `seed` or `source`. + pub kind: String, + pub name: String, + pub materialized: String, + pub incremental_mode: String, + pub incremental_strategy: String, + pub has_cursor: bool, + /// The configured `unique_key` columns, when it is a name or a list of names. + pub unique_key: Option>, + pub columns: Vec, + /// Table-level audits as `(definition name, its string column argument)`. + pub audits: Vec<(String, Option)>, + /// Whether the relation is known to hold at most one row, which the empty key proves. + pub at_most_one_row: bool, +} + +/// One declared column: its name, its audit definition names and whether it is declared non-null. +#[derive(Debug, Clone, Default)] +pub struct DeclaredKeyColumn { + pub name: String, + pub audits: Vec, + pub not_nullable: bool, +} diff --git a/crates/sqlclean/src/sql_quality/tests.rs b/crates/sqlclean/src/sql_quality/tests.rs new file mode 100644 index 0000000..2643da6 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests.rs @@ -0,0 +1,10 @@ +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_json_parses.rs"] +mod json_parses; +#[path = "tests/test_quality_rules.rs"] +mod quality_rules; +#[path = "tests/test_quality_scale.rs"] +mod quality_scale; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean/src/sql_quality/tests/helpers.rs b/crates/sqlclean/src/sql_quality/tests/helpers.rs new file mode 100644 index 0000000..3b8a43d --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests/helpers.rs @@ -0,0 +1,116 @@ +use serde_json::{Value, json}; + +use crate::sql_lint::main::engine::lint_json; +use crate::sql_quality::tests::test_types::QualityRuleTestCase; + +pub(crate) fn lint(test_case: &QualityRuleTestCase) -> Result, String> { + let relation_keys: Value = + serde_json::from_str(test_case.relation_keys).map_err(|error| error.to_string())?; + let request: Value = json!({ + "version": 1, + "sql": test_case.sql, + "dialect": "duckdb", + "enabled_rules": [test_case.rule], + "max_literal_length": 24, + "max_ranking_order_by": 2, + "relation_keys": relation_keys, + }); + let response: Value = serde_json::from_str(&lint_json(&request.to_string())?) + .map_err(|error| error.to_string())?; + Ok(response["diagnostics"] + .as_array() + .cloned() + .unwrap_or_default()) +} + +/// Lint `sql` in `dialect` with only `rule` enabled. +pub(crate) fn lint_dialect(sql: &str, dialect: &str, rule: &str) -> Result, String> { + let request: Value = json!({ + "version": 1, + "sql": sql, + "dialect": dialect, + "enabled_rules": [rule], + }); + let response: Value = serde_json::from_str(&lint_json(&request.to_string())?) + .map_err(|error| error.to_string())?; + Ok(response["diagnostics"] + .as_array() + .cloned() + .unwrap_or_default()) +} + +/// One DuckDB SELECT that parses each of `column_count` payload columns twice. +pub(crate) fn wide_json_select(column_count: usize) -> String { + let projections: Vec = (0..column_count * 2) + .map(|index| format!("json(e.c{})->>'f{index}' AS f{index}", index % column_count)) + .collect(); + format!("SELECT {} FROM events AS e", projections.join(", ")) +} + +pub(crate) fn anchors(sql: &str, diagnostics: &[Value]) -> Vec { + diagnostics + .iter() + .map(|diagnostic| { + let start: usize = diagnostic["start"].as_u64().unwrap_or(0) as usize; + let end: usize = diagnostic["end"].as_u64().unwrap_or(0) as usize; + sql.chars() + .skip(start) + .take(end.saturating_sub(start)) + .collect() + }) + .collect() +} + +pub(crate) fn fixed(sql: &str, diagnostics: &[Value]) -> Option { + let mut edits: Vec<(usize, usize, String)> = diagnostics + .iter() + .filter_map(|diagnostic| { + let fix: &Value = diagnostic.get("fix")?; + Some(( + fix["start"].as_u64()? as usize, + fix["end"].as_u64()? as usize, + fix["replacement"].as_str()?.to_owned(), + )) + }) + .collect(); + edits.sort_by_key(|edit| std::cmp::Reverse(edit.0)); + let applied: bool = !edits.is_empty(); + let mut characters: Vec = sql.chars().collect(); + for (start, end, replacement) in edits { + characters.splice(start..end, replacement.chars()); + } + applied.then(|| characters.into_iter().collect()) +} + +/// A chain of CTEs that each drop their last column and rank without a declared key. +pub(crate) fn wide_cte_chain(cte_count: usize, column_count: usize) -> String { + let columns: Vec = (0..column_count).map(|index| format!("c{index}")).collect(); + let mut ctes: Vec = vec![format!( + "step_0 AS (SELECT {} FROM source_rows AS s)", + columns + .iter() + .map(|column| format!("s.{column}")) + .collect::>() + .join(", ") + )]; + for step in 1..cte_count { + let mut outputs: Vec = columns[..column_count - 1] + .iter() + .map(|column| format!("p.{column}")) + .collect(); + outputs.push(format!( + "ROW_NUMBER() OVER (PARTITION BY p.c0 ORDER BY p.c1) AS c{}", + column_count - 1 + )); + ctes.push(format!( + "step_{step} AS (SELECT {} FROM step_{} AS p)", + outputs.join(", "), + step - 1 + )); + } + format!( + "WITH {} SELECT * FROM step_{}", + ctes.join(",\n"), + cte_count - 1 + ) +} diff --git a/crates/sqlclean/src/sql_quality/tests/test_json_parses.rs b/crates/sqlclean/src/sql_quality/tests/test_json_parses.rs new file mode 100644 index 0000000..570a272 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests/test_json_parses.rs @@ -0,0 +1,282 @@ +use std::time::Instant; + +use serde_json::Value; + +use crate::sql_quality::tests::helpers::{anchors, lint_dialect, wide_json_select}; +use crate::sql_quality::tests::test_types::{JsonParseScaleTestCase, JsonParseTestCase}; + +const RULE: &str = "SQC045"; + +#[test] +fn given_json_parse_calls_when_linting_then_repeats_in_one_select_are_reported() +-> Result<(), String> { + let test_cases = [ + JsonParseTestCase { + description: "same column parsed twice reports the second call", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.value):order_id AS order_id, TRY_PARSE_JSON(e.value):status AS status FROM events AS e", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "the finding is located at the second call", + dialect: "snowflake", + sql: "SELECT try_parse_json(value):a AS a, TRY_PARSE_JSON(value):b AS b FROM events", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "three parses give one finding with the count", + dialect: "snowflake", + sql: "SELECT PARSE_JSON(value):a AS a, PARSE_JSON(value):b AS b FROM events WHERE PARSE_JSON(value):c IS NOT NULL", + expected_anchors: &["PARSE_JSON"], + expected_details: &["PARSE_JSON(value) is parsed 3 times in one SELECT."], + }, + JsonParseTestCase { + description: "column casing and wrappers do not hide a repeat", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(LOWER(E.Value)):a AS a, TRY_PARSE_JSON((CAST(TRIM(e.value) AS VARCHAR))):b AS b, TRY_PARSE_JSON(COALESCE(UPPER(e.VALUE))):c AS c FROM events AS e", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(e.value) is parsed 3 times in one SELECT."], + }, + JsonParseTestCase { + description: "multi-argument COALESCE is not a plain column", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(COALESCE(e.value, e.backup)):a AS a, TRY_PARSE_JSON(COALESCE(e.value, e.fallback)):b AS b, TRY_PARSE_JSON(e.value):c AS c FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "TRIM with characters is not a plain column", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(TRIM(e.value, ' ')):a AS a, TRY_PARSE_JSON(e.value):b AS b FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "quoted names differing in case are different columns", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.\"Value\"):a AS a, TRY_PARSE_JSON(e.\"value\"):b AS b FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "quoted and unquoted names are not merged", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.\"value\"):a AS a, TRY_PARSE_JSON(e.value):b AS b FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "quoted qualifiers differing in case are different relations", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(\"E\".value):a AS a, TRY_PARSE_JSON(\"e\".value):b AS b FROM events AS \"E\" INNER JOIN events AS \"e\" ON \"E\".id = \"e\".id", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "identical quoted names are reported as written", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(\"E\".\"Value\"):a AS a, TRY_PARSE_JSON(\"E\".\"Value\"):b AS b FROM events AS \"E\"", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(\"E\".\"Value\") is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "bigquery quoted names keep backticks", + dialect: "bigquery", + sql: "SELECT JSON_VALUE(PARSE_JSON(e.`Value`), '$.a') AS a, JSON_VALUE(PARSE_JSON(e.`Value`), '$.b') AS b FROM events AS e", + expected_anchors: &["PARSE_JSON"], + expected_details: &["PARSE_JSON(e.`Value`) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "snowflake quoted upper case matches the folded unquoted name", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.\"VALUE\"):a AS a, TRY_PARSE_JSON(e.value):b AS b FROM events AS e", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "duckdb quoted names are case-insensitive", + dialect: "duckdb", + sql: "SELECT json(e.\"Value\") -> 'a' AS a, json(e.value) -> 'b' AS b FROM events AS e", + expected_anchors: &["json"], + expected_details: &["JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "bigquery backticked names are case-insensitive", + dialect: "bigquery", + sql: "SELECT JSON_VALUE(PARSE_JSON(e.`Value`), '$.a') AS a, JSON_VALUE(PARSE_JSON(e.value), '$.b') AS b FROM events AS e", + expected_anchors: &["PARSE_JSON"], + expected_details: &["PARSE_JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "databricks backticked names are case-insensitive", + dialect: "databricks", + sql: "SELECT try_parse_json(e.`v`) AS a, try_parse_json(e.V) AS b FROM events AS e", + expected_anchors: &["try_parse_json"], + expected_details: &["TRY_PARSE_JSON(e.V) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "references are grouped as written", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(value):a AS a, TRY_PARSE_JSON(e.value):b AS b, TRY_PARSE_JSON(events.value):c AS c FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "different columns pass", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.payload):a AS a, TRY_PARSE_JSON(e.metadata):b AS b FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "same column name on different tables passes", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(o.payload):a AS a, TRY_PARSE_JSON(c.payload):b AS b FROM orders AS o INNER JOIN customers AS c ON o.customer_id = c.customer_id", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "different parse functions are not merged", + dialect: "snowflake", + sql: "SELECT PARSE_JSON(value):a AS a, TRY_PARSE_JSON(value):b AS b FROM events", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "one parse per CTE scope passes", + dialect: "snowflake", + sql: "WITH parsed AS (SELECT TRY_PARSE_JSON(value) AS payload FROM events), final AS (SELECT TRY_PARSE_JSON(value) AS payload FROM events) SELECT payload FROM final", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "nested SELECT is its own scope", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(e.value):a AS a, (SELECT MAX(TRY_PARSE_JSON(x.value):b) FROM events AS x) AS b FROM events AS e", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "repeat inside a subquery is reported in that scope", + dialect: "snowflake", + sql: "SELECT e.order_id FROM events AS e WHERE e.order_id IN (SELECT TRY_PARSE_JSON(value):id FROM events WHERE TRY_PARSE_JSON(value):ok)", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "union branches are separate scopes", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(value) AS payload FROM events UNION ALL SELECT TRY_PARSE_JSON(value) AS payload FROM events", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "a repeat inside one CTE body is reported", + dialect: "snowflake", + sql: "WITH parsed AS (SELECT TRY_PARSE_JSON(value):a AS a, TRY_PARSE_JSON(value):b AS b FROM events) SELECT a, b FROM parsed", + expected_anchors: &["TRY_PARSE_JSON"], + expected_details: &["TRY_PARSE_JSON(value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "non-column arguments pass", + dialect: "snowflake", + sql: "SELECT TRY_PARSE_JSON(value || '') AS a, TRY_PARSE_JSON(value || '') AS b FROM events", + expected_anchors: &[], + expected_details: &[], + }, + JsonParseTestCase { + description: "bigquery safe parse is matched", + dialect: "bigquery", + sql: "SELECT JSON_VALUE(SAFE.PARSE_JSON(e.value), '$.a') AS a, JSON_VALUE(SAFE.PARSE_JSON(e.value), '$.b') AS b FROM events AS e", + expected_anchors: &["PARSE_JSON"], + expected_details: &["SAFE.PARSE_JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "databricks try parse is matched", + dialect: "databricks", + sql: "SELECT try_parse_json(value) AS a, try_parse_json(value) AS b FROM events", + expected_anchors: &["try_parse_json"], + expected_details: &["TRY_PARSE_JSON(value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "duckdb json is matched", + dialect: "duckdb", + sql: "SELECT json(e.value)->>'a' AS a, json(e.value)->>'b' AS b FROM events AS e", + expected_anchors: &["json"], + expected_details: &["JSON(e.value) is parsed 2 times in one SELECT."], + }, + JsonParseTestCase { + description: "dialects without an equivalent are not checked", + dialect: "postgres", + sql: "SELECT CAST(value AS JSONB) AS a, CAST(value AS JSONB) AS b FROM events", + expected_anchors: &[], + expected_details: &[], + }, + ]; + for test_case in &test_cases { + let diagnostics: Vec = lint_dialect(test_case.sql, test_case.dialect, RULE)?; + assert_eq!( + anchors(test_case.sql, &diagnostics), + test_case.expected_anchors, + "{}", + test_case.description + ); + let details: Vec = diagnostics + .iter() + .map(|diagnostic| { + diagnostic["remediation"] + .as_str() + .unwrap_or_default() + .split_inclusive(". ") + .next() + .unwrap_or_default() + .trim_end() + .to_owned() + }) + .collect(); + assert_eq!( + details, test_case.expected_details, + "{}", + test_case.description + ); + assert!( + diagnostics + .iter() + .all(|diagnostic| diagnostic["fix"].is_null()), + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_wide_select_with_many_repeated_parses_when_linting_then_it_stays_linear() +-> Result<(), String> { + let test_cases = [JsonParseScaleTestCase { + description: "3,000 projections parsing 1,500 payload columns twice each", + column_count: 1_500, + expected_findings: 1_500, + expected_max_seconds: 10.0, + }]; + for test_case in &test_cases { + let started: Instant = Instant::now(); + let diagnostics: Vec = + lint_dialect(&wide_json_select(test_case.column_count), "duckdb", RULE)?; + let elapsed: f64 = started.elapsed().as_secs_f64(); + assert_eq!( + diagnostics.len(), + test_case.expected_findings, + "{}", + test_case.description + ); + assert!( + elapsed <= test_case.expected_max_seconds, + "{}: {elapsed:.2}s", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_quality/tests/test_quality_rules.rs b/crates/sqlclean/src/sql_quality/tests/test_quality_rules.rs new file mode 100644 index 0000000..7ad2349 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests/test_quality_rules.rs @@ -0,0 +1,403 @@ +use serde_json::Value; + +use crate::sql_quality::tests::helpers::{anchors, fixed, lint}; +use crate::sql_quality::tests::test_types::{QualityRemediationTestCase, QualityRuleTestCase}; + +#[test] +fn given_quality_rule_inputs_when_linting_then_findings_and_fixes_match() -> Result<(), String> { + let test_cases = [ + QualityRuleTestCase { + description: "long literal is reported without a split fix", + sql: "SELECT 'pending orders awaiting shipment' AS label", + rule: "SQC044", + relation_keys: "{}", + expected_anchors: &["'pending orders awaiting shipment'"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "short literal passes", + sql: "SELECT 'pending' AS label", + rule: "SQC044", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "typed literal is reported without a fix", + sql: "SELECT TIMESTAMP '2026-04-01 10:00:00.000000000' AS ordered_at", + rule: "SQC044", + relation_keys: "{}", + expected_anchors: &["'2026-04-01 10:00:00.000000000'"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "ranking sort above the cap", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.a, o.b, o.order_id) AS rn FROM orders AS o", + rule: "SQC043", + relation_keys: "{}", + expected_anchors: &["ROW_NUMBER"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "ranking sort within the cap", + sql: "SELECT RANK() OVER (ORDER BY o.a, o.order_id) AS rn FROM orders AS o", + rule: "SQC043", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "ordering covers a declared key", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.ordered_at, o.order_id) AS rn FROM orders AS o", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]]}"#, + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "ordering without a key is unproven", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.ordered_at) AS rn FROM orders AS o", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]]}"#, + expected_anchors: &["ROW_NUMBER"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "key proven through a CTE and a one-to-one join", + sql: "WITH lines AS (SELECT l.line_id, l.order_id FROM order_lines AS l), final AS (SELECT l.line_id, o.customer_id, ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY l.line_id) AS rn FROM lines AS l INNER JOIN orders AS o ON l.order_id = o.order_id) SELECT f.line_id, f.customer_id, f.rn FROM final AS f", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]], "order_lines": [["line_id"]]}"#, + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "key lost through a fan-out join", + sql: "WITH final AS (SELECT o.order_id, ROW_NUMBER() OVER (ORDER BY o.order_id) AS rn FROM orders AS o INNER JOIN order_lines AS l ON l.order_id = o.order_id) SELECT f.order_id, f.rn FROM final AS f", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]], "order_lines": [["line_id"]]}"#, + expected_anchors: &["ROW_NUMBER"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "GROUP BY grain is a key", + sql: "WITH totals AS (SELECT o.customer_id, SUM(o.amount) AS amount FROM orders AS o GROUP BY o.customer_id), final AS (SELECT t.customer_id, ROW_NUMBER() OVER (ORDER BY t.amount, t.customer_id) AS rn FROM totals AS t) SELECT f.customer_id, f.rn FROM final AS f", + rule: "SQC018", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "positional GROUP BY grain is a key", + sql: "WITH totals AS (SELECT o.customer_id, SUM(o.amount) AS amount FROM orders AS o GROUP BY 1), final AS (SELECT t.customer_id, ROW_NUMBER() OVER (ORDER BY t.amount, t.customer_id) AS rn FROM totals AS t) SELECT f.customer_id, f.rn FROM final AS f", + rule: "SQC018", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a later rn = 1 filter makes the partition a key", + sql: "WITH ranked AS (SELECT o.order_id, o.customer_id, ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.order_id) AS rn FROM orders AS o), latest AS (SELECT r.customer_id, r.order_id FROM ranked AS r WHERE r.rn = 1), final AS (SELECT l.customer_id, ROW_NUMBER() OVER (ORDER BY l.customer_id) AS position FROM latest AS l) SELECT f.customer_id, f.position FROM final AS f", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]]}"#, + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a QUALIFY window cannot prove itself", + sql: "SELECT o.customer_id, o.order_id FROM orders AS o QUALIFY ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.amount DESC) = 1", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]]}"#, + expected_anchors: &["ROW_NUMBER"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a QUALIFY partition is a key for later steps", + sql: "WITH latest AS (SELECT o.customer_id, o.order_id FROM orders AS o QUALIFY ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.order_id) = 1), final AS (SELECT l.customer_id, ROW_NUMBER() OVER (ORDER BY l.customer_id) AS position FROM latest AS l) SELECT f.customer_id, f.position FROM final AS f", + rule: "SQC018", + relation_keys: r#"{"orders": [["order_id"]]}"#, + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "RANK ties are deterministic", + sql: "SELECT RANK() OVER (ORDER BY o.ordered_at) AS rn FROM orders AS o", + rule: "SQC018", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "unused CTE output is removed", + sql: "WITH orders AS (SELECT o.order_id, o.status, o.note FROM raw_orders AS o), final AS (SELECT r.order_id, r.status FROM orders AS r) SELECT f.order_id, f.status FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["o.note"], + expected_fixed_sql: Some( + "WITH orders AS (SELECT o.order_id, o.status FROM raw_orders AS o), final AS (SELECT r.order_id, r.status FROM orders AS r) SELECT f.order_id, f.status FROM final AS f", + ), + }, + QualityRuleTestCase { + description: "star pass-through into the final CTE keeps every column", + sql: "WITH orders AS (SELECT o.order_id, o.note FROM raw_orders AS o), final AS (SELECT * FROM orders) SELECT * FROM final", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "star pass-through follows the reader's own reads", + sql: "WITH orders AS (SELECT o.order_id, o.note FROM raw_orders AS o), staged AS (SELECT * FROM orders), final AS (SELECT s.order_id FROM staged AS s) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["o.note"], + expected_fixed_sql: Some( + "WITH orders AS (SELECT o.order_id FROM raw_orders AS o), staged AS (SELECT * FROM orders), final AS (SELECT s.order_id FROM staged AS s) SELECT f.order_id FROM final AS f", + ), + }, + QualityRuleTestCase { + description: "a same-named cast of a source column is not a lateral read", + sql: "WITH orders AS (SELECT o.order_id, CAST(note AS VARCHAR) AS note, o.note || 'x' AS tagged FROM raw_orders AS o), final AS (SELECT r.order_id, r.tagged FROM orders AS r) SELECT f.order_id, f.tagged FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["CAST(note AS VARCHAR) AS note"], + expected_fixed_sql: Some( + "WITH orders AS (SELECT o.order_id, o.note || 'x' AS tagged FROM raw_orders AS o), final AS (SELECT r.order_id, r.tagged FROM orders AS r) SELECT f.order_id, f.tagged FROM final AS f", + ), + }, + QualityRuleTestCase { + description: "an interpolated expression in a reader may read any column", + sql: "WITH orders AS (SELECT o.order_id, o.status FROM raw_orders AS o), final AS (SELECT r.order_id FROM orders AS r WHERE __sqlbuild_audit_parameter_0__) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "an interpolated output is never reported", + sql: "WITH orders AS (SELECT o.order_id, __SQB_LINT_0002 FROM raw_orders AS o), final AS (SELECT r.order_id FROM orders AS r) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "DISTINCT CTE reports without a fix", + sql: "WITH orders AS (SELECT DISTINCT o.order_id, o.note FROM raw_orders AS o), final AS (SELECT r.order_id FROM orders AS r) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["o.note"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "columns read in later join and filter clauses are used", + sql: "WITH orders AS (SELECT o.order_id, o.customer_id, o.status FROM raw_orders AS o), final AS (SELECT c.name FROM orders AS r INNER JOIN customers AS c ON r.customer_id = c.customer_id WHERE r.status = 'open' QUALIFY ROW_NUMBER() OVER (PARTITION BY r.order_id ORDER BY c.name) = 1) SELECT f.name FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a column read only through a nested star is used", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), final AS (SELECT g.order_id, HASH(g.*) AS row_hash FROM g) SELECT f.order_id, f.row_hash FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a whole-row reference reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), final AS (SELECT g.order_id, HASH(g) AS row_hash FROM g) SELECT f.order_id, f.row_hash FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "COUNT(DISTINCT row) reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), final AS (SELECT COUNT(DISTINCT g) AS n FROM g) SELECT f.n FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "COLUMNS(*) reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), final AS (SELECT g.order_id FROM g WHERE COLUMNS(*) IS NOT NULL) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "COLUMNS with a pattern reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), final AS (SELECT MAX(COLUMNS('order.*')) FROM g) SELECT * FROM final", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "DISTINCT star pass-through reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), staged AS (SELECT DISTINCT * FROM g), final AS (SELECT s.order_id FROM staged AS s) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a positional ORDER BY over a star pass-through reads every column", + sql: "WITH g AS (SELECT r.order_id, r.amount FROM raw_orders AS r), staged AS (SELECT * FROM g ORDER BY 2 LIMIT 1), final AS (SELECT s.order_id FROM staged AS s) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "an output alias read in WHERE or GROUP BY is used", + sql: "WITH g AS (SELECT r.order_id, r.amount * 2 AS doubled, r.customer_id % 2 AS bucket FROM raw_orders AS r WHERE doubled > 1 GROUP BY r.order_id, doubled, bucket), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "GROUP BY ALL grouping column is reported without a fix", + sql: "WITH g AS (SELECT r.customer_id, r.status, SUM(r.amount) AS total FROM raw_orders AS r GROUP BY ALL), final AS (SELECT g.customer_id, g.total FROM g) SELECT f.customer_id, f.total FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["r.status"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "an aggregate in an ungrouped select is reported without a fix", + sql: "WITH g AS (SELECT 'all' AS label, COUNT(*) AS n FROM raw_orders AS r), final AS (SELECT g.label FROM g) SELECT f.label FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["COUNT(*) AS n"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a set-returning projection is reported without a fix", + sql: "WITH g AS (SELECT r.order_id, UNNEST(r.tags) AS tag FROM raw_orders AS r), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["UNNEST(r.tags) AS tag"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "ORDER BY ALL with LIMIT is reported without a fix", + sql: "WITH g AS (SELECT r.order_id, r.customer_id FROM raw_orders AS r ORDER BY ALL LIMIT 1), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["r.customer_id"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "DISTINCT ON is reported without a fix", + sql: "WITH g AS (SELECT DISTINCT ON (r.customer_id) r.order_id, r.amount FROM raw_orders AS r ORDER BY r.customer_id, r.order_id), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["r.amount"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a volatile projection is reported without a fix", + sql: "WITH g AS (SELECT r.order_id, RANDOM() AS draw FROM raw_orders AS r), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["RANDOM() AS draw"], + expected_fixed_sql: None, + }, + QualityRuleTestCase { + description: "a deterministic scalar projection is removed", + sql: "WITH g AS (SELECT r.order_id, COALESCE(UPPER(r.status), 'none') AS label FROM raw_orders AS r), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + rule: "SQC042", + relation_keys: "{}", + expected_anchors: &["COALESCE(UPPER(r.status), 'none') AS label"], + expected_fixed_sql: Some( + "WITH g AS (SELECT r.order_id FROM raw_orders AS r), final AS (SELECT g.order_id FROM g) SELECT f.order_id FROM final AS f", + ), + }, + ]; + for test_case in &test_cases { + let diagnostics: Vec = lint(test_case)?; + assert_eq!( + anchors(test_case.sql, &diagnostics), + test_case.expected_anchors, + "{}", + test_case.description + ); + assert_eq!( + fixed(test_case.sql, &diagnostics).as_deref(), + test_case.expected_fixed_sql, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_threshold_findings_when_linting_then_remediation_shows_the_exact_setting() +-> Result<(), String> { + let test_cases = [ + QualityRemediationTestCase { + description: "long literal points at shortening it and the length setting", + sql: "SELECT 'pending orders awaiting shipment' AS label", + rule: "SQC044", + expected_fragments: &[ + "Shorten the literal", + "set this in sqlclean.toml:", + "the current value is 24", + "[rules.thresholds]\n max_literal_length = 32", + ], + }, + QualityRemediationTestCase { + description: "long ranking sort points at a row key and the sort setting", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.a, o.b, o.order_id) AS rn FROM orders AS o", + rule: "SQC043", + expected_fragments: &[ + "unique row key as the final tie-breaker", + "HASH(source_partition, source_offset, line_index) AS row_key", + "the current value is 2", + "[rules.thresholds]\n max_ranking_order_by = 3", + ], + }, + QualityRemediationTestCase { + description: "unproven ranking points at a unique tie-breaker and a built row key", + sql: "SELECT ROW_NUMBER() OVER (PARTITION BY o.customer_id ORDER BY o.a) AS rn FROM orders AS o", + rule: "SQC018", + expected_fragments: &[ + "cover a unique key of the ranked rows", + "a unique, non-null tie-breaker column", + "HASH(source_partition, source_offset, line_index) AS row_key", + ], + }, + ]; + for test_case in &test_cases { + let diagnostics: Vec = lint(&QualityRuleTestCase { + description: test_case.description, + sql: test_case.sql, + rule: test_case.rule, + relation_keys: "{}", + expected_anchors: &[], + expected_fixed_sql: None, + })?; + let remediation: &str = diagnostics + .first() + .and_then(|diagnostic| diagnostic["remediation"].as_str()) + .ok_or_else(|| format!("{}: no finding", test_case.description))?; + let present: Vec<&str> = test_case + .expected_fragments + .iter() + .copied() + .filter(|fragment| remediation.contains(fragment)) + .collect(); + assert_eq!( + present, test_case.expected_fragments, + "{}: {remediation}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_quality/tests/test_quality_scale.rs b/crates/sqlclean/src/sql_quality/tests/test_quality_scale.rs new file mode 100644 index 0000000..46f8ad3 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests/test_quality_scale.rs @@ -0,0 +1,43 @@ +use std::time::Instant; + +use serde_json::{Value, json}; + +use crate::sql_lint::main::engine::lint_json; +use crate::sql_quality::tests::helpers::wide_cte_chain; +use crate::sql_quality::tests::test_types::QualityScaleTestCase; + +#[test] +fn given_many_wide_ctes_with_findings_when_linting_then_quality_rules_stay_linear() +-> Result<(), String> { + let test_cases = [QualityScaleTestCase { + description: "1,500 wide CTEs, each dropping a column and ranking without a key", + cte_count: 1_500, + column_count: 24, + expected_findings: 2_998, + expected_max_seconds: 20.0, + }]; + for test_case in &test_cases { + let request: Value = json!({ + "version": 1, + "sql": wide_cte_chain(test_case.cte_count, test_case.column_count), + "dialect": "duckdb", + "enabled_rules": ["SQC018", "SQC042", "SQC043", "SQC044"], + }); + let started: Instant = Instant::now(); + let response: Value = serde_json::from_str(&lint_json(&request.to_string())?) + .map_err(|error| error.to_string())?; + let elapsed: f64 = started.elapsed().as_secs_f64(); + assert_eq!( + response["diagnostics"].as_array().map_or(0, Vec::len), + test_case.expected_findings, + "{}", + test_case.description + ); + assert!( + elapsed <= test_case.expected_max_seconds, + "{}: {elapsed:.2}s", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_quality/tests/test_types.rs b/crates/sqlclean/src/sql_quality/tests/test_types.rs new file mode 100644 index 0000000..bc80b42 --- /dev/null +++ b/crates/sqlclean/src/sql_quality/tests/test_types.rs @@ -0,0 +1,38 @@ +pub(crate) struct QualityRuleTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) rule: &'static str, + pub(crate) relation_keys: &'static str, + pub(crate) expected_anchors: &'static [&'static str], + pub(crate) expected_fixed_sql: Option<&'static str>, +} + +pub(crate) struct QualityScaleTestCase { + pub(crate) description: &'static str, + pub(crate) cte_count: usize, + pub(crate) column_count: usize, + pub(crate) expected_findings: usize, + pub(crate) expected_max_seconds: f64, +} + +pub(crate) struct QualityRemediationTestCase { + pub(crate) description: &'static str, + pub(crate) sql: &'static str, + pub(crate) rule: &'static str, + pub(crate) expected_fragments: &'static [&'static str], +} + +pub(crate) struct JsonParseTestCase { + pub(crate) description: &'static str, + pub(crate) dialect: &'static str, + pub(crate) sql: &'static str, + pub(crate) expected_anchors: &'static [&'static str], + pub(crate) expected_details: &'static [&'static str], +} + +pub(crate) struct JsonParseScaleTestCase { + pub(crate) description: &'static str, + pub(crate) column_count: usize, + pub(crate) expected_findings: usize, + pub(crate) expected_max_seconds: f64, +} diff --git a/crates/sqlclean/src/sql_quality/types.rs b/crates/sqlclean/src/sql_quality/types.rs new file mode 100644 index 0000000..717e03a --- /dev/null +++ b/crates/sqlclean/src/sql_quality/types.rs @@ -0,0 +1,6 @@ +use std::collections::BTreeSet; + +/// Column sets that are unique for one relation, by lower-cased output column name. +pub(crate) type Keys = Vec>; +/// A unique column set over a select's joined input, as `(source alias, column)` pairs. +pub(crate) type InputKey = BTreeSet<(String, String)>; diff --git a/crates/sqlclean/src/sql_tokens/_helpers/builtin_functions.rs b/crates/sqlclean/src/sql_tokens/_helpers/builtin_functions.rs new file mode 100644 index 0000000..852563f --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/_helpers/builtin_functions.rs @@ -0,0 +1,151 @@ +//! Function names a dialect recognises as built-ins, from the SQL library's own metadata. + +use std::collections::{HashMap, HashSet}; +use std::sync::LazyLock; + +use crate::sql_tokens::constants::CALL_SYNTAX_FUNCTIONS; +use polyglot_sql::{Dialect, DialectType, Expression}; +use polyglot_sql_function_catalogs::types::{builtin_arity, type_signature}; +use polyglot_sql_function_catalogs::{ + CatalogSink, FunctionNameCase, FunctionSignature, register_enabled_catalogs, +}; + +const DUCKDB_CATALOG: &str = "duckdb"; + +static CATALOG_FUNCTIONS: LazyLock>> = LazyLock::new(|| { + let mut sink = NameSink::default(); + register_enabled_catalogs(&mut sink); + sink.names +}); +static SNOWFLAKE_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::Snowflake)); +static BIGQUERY_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::BigQuery)); +static POSTGRES_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::PostgreSQL)); +static DATABRICKS_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::Databricks)); +static TSQL_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::TSQL)); +static GENERIC_TYPED: LazyLock> = + LazyLock::new(|| parser_typed_names(DialectType::Generic)); + +/// Return whether `upper_name(...)` calls a function the dialect defines. +pub(crate) fn is_builtin_function(dialect: DialectType, upper_name: &str) -> bool { + if CALL_SYNTAX_FUNCTIONS.contains(&upper_name) { + return true; + } + let lower_name = upper_name.to_ascii_lowercase(); + let typed = match dialect { + DialectType::DuckDB => return catalogued(DUCKDB_CATALOG, &lower_name), + DialectType::Snowflake => &SNOWFLAKE_TYPED, + DialectType::BigQuery => &BIGQUERY_TYPED, + DialectType::PostgreSQL => &POSTGRES_TYPED, + DialectType::Databricks => &DATABRICKS_TYPED, + DialectType::TSQL => &TSQL_TYPED, + _ => &GENERIC_TYPED, + }; + typed.contains(&lower_name) || catalogued(catalog_key(dialect), &lower_name) +} + +/// Every catalogued or parser-typed built-in function name of `dialect`, lower-cased. +#[cfg(any(test, feature = "test-support"))] +pub(crate) fn builtin_function_names(dialect: DialectType) -> Vec { + let typed: &HashSet = match dialect { + DialectType::DuckDB => &HashSet::new(), + DialectType::Snowflake => &SNOWFLAKE_TYPED, + DialectType::BigQuery => &BIGQUERY_TYPED, + DialectType::PostgreSQL => &POSTGRES_TYPED, + DialectType::Databricks => &DATABRICKS_TYPED, + DialectType::TSQL => &TSQL_TYPED, + _ => &GENERIC_TYPED, + }; + let key = match dialect { + DialectType::DuckDB => DUCKDB_CATALOG, + _ => catalog_key(dialect), + }; + let catalogued = CATALOG_FUNCTIONS.get(key).into_iter().flatten(); + let mut names: Vec = typed.iter().chain(catalogued).cloned().collect(); + names.sort_unstable(); + names.dedup(); + names +} + +fn catalog_key(dialect: DialectType) -> &'static str { + match dialect { + DialectType::Snowflake => "snowflake", + DialectType::BigQuery => "bigquery", + DialectType::PostgreSQL => "postgres", + _ => "", + } +} + +fn catalogued(key: &str, lower_name: &str) -> bool { + CATALOG_FUNCTIONS + .get(key) + .is_some_and(|names| names.contains(lower_name)) + || builtin_arity(key, lower_name).is_some() + || type_signature(key, lower_name).is_some() +} + +/// Catalogued function names whose call the dialect's parser turns into a specific expression. +fn parser_typed_names(dialect: DialectType) -> HashSet { + let parser = Dialect::get(dialect); + let mut typed: HashSet = HashSet::new(); + for name in CATALOG_FUNCTIONS.get(DUCKDB_CATALOG).into_iter().flatten() { + if is_typed_call(&parser, name) { + typed.insert(name.clone()); + } + } + typed +} + +fn is_typed_call(parser: &Dialect, lower_name: &str) -> bool { + if !lower_name + .chars() + .all(|character| character.is_ascii_alphanumeric() || character == '_') + { + return false; + } + let Ok(statements) = parser.parse(&format!("SELECT {lower_name}(probe)")) else { + return false; + }; + let Some(Expression::Select(select)) = statements.first() else { + return false; + }; + select.expressions.first().is_some_and(|call| { + !matches!( + call, + Expression::Function(_) | Expression::Anonymous(_) | Expression::Column(_) + ) + }) +} + +#[derive(Default)] +struct NameSink { + names: HashMap<&'static str, HashSet>, +} + +impl CatalogSink for NameSink { + fn set_dialect_name_case(&mut self, _dialect: &'static str, _name_case: FunctionNameCase) {} + + fn set_function_name_case( + &mut self, + _dialect: &'static str, + _function_name: &str, + _name_case: FunctionNameCase, + ) { + } + + fn register( + &mut self, + dialect: &'static str, + function_name: &str, + _signatures: Vec, + ) { + self.names + .entry(dialect) + .or_default() + .insert(function_name.to_ascii_lowercase()); + } +} diff --git a/crates/sqlclean/src/sql_tokens/_helpers/mod.rs b/crates/sqlclean/src/sql_tokens/_helpers/mod.rs new file mode 100644 index 0000000..a2df8fb --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod builtin_functions; +pub mod reserved_keywords; +pub mod token_context; diff --git a/crates/sqlclean/src/sql_tokens/_helpers/reserved_keywords.rs b/crates/sqlclean/src/sql_tokens/_helpers/reserved_keywords.rs new file mode 100644 index 0000000..ccf9004 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/_helpers/reserved_keywords.rs @@ -0,0 +1,43 @@ +//! Per-dialect reserved keywords, which can never be unquoted identifiers in that dialect. + +use std::collections::HashSet; +use std::sync::LazyLock; + +use polyglot_sql::DialectType; + +static DUCKDB: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/duckdb.txt"))); +static POSTGRES: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/postgres.txt"))); +static BIGQUERY: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/bigquery.txt"))); +static SNOWFLAKE: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/snowflake.txt"))); +static TSQL: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/tsql.txt"))); +static DATABRICKS: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/databricks.txt"))); +static GENERIC: LazyLock> = + LazyLock::new(|| keyword_set(include_str!("../reserved_keywords/generic.txt"))); + +/// Return whether the upper-cased word is a reserved keyword of `dialect`. +pub(crate) fn is_reserved_keyword(dialect: DialectType, upper_word: &str) -> bool { + let keywords: &HashSet = match dialect { + DialectType::DuckDB => &DUCKDB, + DialectType::PostgreSQL => &POSTGRES, + DialectType::BigQuery => &BIGQUERY, + DialectType::Snowflake => &SNOWFLAKE, + DialectType::TSQL => &TSQL, + DialectType::Databricks => &DATABRICKS, + _ => &GENERIC, + }; + keywords.contains(upper_word) +} + +fn keyword_set(data: &str) -> HashSet { + data.lines() + .map(str::trim) + .filter(|line| !line.is_empty() && !line.starts_with('#')) + .map(str::to_ascii_uppercase) + .collect() +} diff --git a/crates/sqlclean/src/sql_tokens/_helpers/token_context.rs b/crates/sqlclean/src/sql_tokens/_helpers/token_context.rs new file mode 100644 index 0000000..fabb747 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/_helpers/token_context.rs @@ -0,0 +1,30 @@ +//! Positional context of one authored token among its neighbours. + +use polyglot_sql::DialectType; + +use crate::sql_tokens::_helpers::builtin_functions::is_builtin_function; +use crate::sql_tokens::_helpers::reserved_keywords::is_reserved_keyword; +use crate::sql_tokens::constants::CALL_OPENER; +use crate::sql_tokens::main::is_unquoted_word::is_unquoted_word; + +pub(crate) struct TokenContext<'a> { + pub(crate) raws: &'a [String], + pub(crate) uppers: &'a [String], + pub(crate) index: usize, +} + +impl TokenContext<'_> { + /// Whether the token is a reserved keyword or a built-in function name at a call site. + pub(crate) fn is_foldable(&self, dialect: DialectType) -> bool { + if !is_unquoted_word(&self.raws[self.index]) { + return false; + } + let upper = self.uppers[self.index].as_str(); + is_reserved_keyword(dialect, upper) + || (self.next() == Some(CALL_OPENER) && is_builtin_function(dialect, upper)) + } + + fn next(&self) -> Option<&str> { + self.uppers.get(self.index + 1).map(String::as_str) + } +} diff --git a/crates/sqlclean/src/sql_tokens/constants.rs b/crates/sqlclean/src/sql_tokens/constants.rs new file mode 100644 index 0000000..6036eb9 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/constants.rs @@ -0,0 +1,35 @@ +//! Token positions that decide which authored words may change letter case. + +use polyglot_sql::tokens::TokenType; + +/// Column aliases after `AS` may be any keyword in DuckDB and PostgreSQL. +pub(crate) const ALIAS_KEYWORD: &str = "AS"; +/// The token that turns a preceding name into a function call. +pub(crate) const CALL_OPENER: &str = "("; +/// Field names after `.`, path keys after `:`, and parameter names after `@` or `$`. +pub(crate) const NAME_PREFIXES: [&str; 4] = [".", ":", "@", "$"]; +/// Built-in calls with keyword syntax inside their parentheses, which catalogues do not list. +pub const CALL_SYNTAX_FUNCTIONS: [&str; 6] = [ + "CAST", + "TRY_CAST", + "SAFE_CAST", + "EXTRACT", + "CONVERT", + "TRY_CONVERT", +]; + +pub(crate) const PROJECTION_ENDS: [TokenType; 13] = [ + TokenType::From, + TokenType::Where, + TokenType::Group, + TokenType::GroupBy, + TokenType::Having, + TokenType::Qualify, + TokenType::Order, + TokenType::OrderBy, + TokenType::Limit, + TokenType::Window, + TokenType::Union, + TokenType::Except, + TokenType::Intersect, +]; diff --git a/crates/sqlclean/src/sql_tokens/main/builtin_function_names.rs b/crates/sqlclean/src/sql_tokens/main/builtin_function_names.rs new file mode 100644 index 0000000..ec0c9e4 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/builtin_function_names.rs @@ -0,0 +1,6 @@ +use polyglot_sql::DialectType; + +/// Every catalogued or parser-typed built-in function name of `dialect`, lower-cased. +pub fn builtin_function_names(dialect: DialectType) -> Vec { + crate::sql_tokens::_helpers::builtin_functions::builtin_function_names(dialect) +} diff --git a/crates/sqlclean/src/sql_tokens/main/canonical_tokens.rs b/crates/sqlclean/src/sql_tokens/main/canonical_tokens.rs new file mode 100644 index 0000000..d1bfb83 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/canonical_tokens.rs @@ -0,0 +1,25 @@ +//! Canonical token stream of SQL text under one dialect tokenizer. + +use polyglot_sql::Dialect; +use polyglot_sql::tokens::Token; + +use crate::sql_tokens::main::case_folding::foldable_tokens; +use crate::sql_tokens::main::token_texts::token_texts; +use crate::sql_tokens::models::CanonicalToken; + +/// Drop whitespace and comments from tokenized `sql` and upper-case case-insensitive tokens. +pub fn canonical_tokens( + sql: &str, + tokens: &[Token], + dialect: &Dialect, +) -> Result, String> { + let raws = token_texts(sql, tokens)?; + let foldable = foldable_tokens(&raws, tokens, dialect); + Ok(raws + .into_iter() + .zip(foldable) + .map(|(raw, fold)| CanonicalToken { + text: if fold { raw.to_ascii_uppercase() } else { raw }, + }) + .collect()) +} diff --git a/crates/sqlclean/src/sql_tokens/main/case_folding.rs b/crates/sqlclean/src/sql_tokens/main/case_folding.rs new file mode 100644 index 0000000..fc29290 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/case_folding.rs @@ -0,0 +1,23 @@ +//! Which authored tokens are case-insensitive keywords or built-in function names. + +use polyglot_sql::Dialect; +use polyglot_sql::tokens::Token; + +use crate::sql_tokens::_helpers::token_context::TokenContext; +use crate::sql_tokens::main::name_positions::name_positions; + +/// Return, for every token, whether the formatter may recase it and the fingerprint folds it. +pub fn foldable_tokens(raws: &[String], tokens: &[Token], dialect: &Dialect) -> Vec { + let uppers: Vec = raws.iter().map(|raw| raw.to_ascii_uppercase()).collect(); + let names = name_positions(&uppers); + let mut foldable = Vec::with_capacity(tokens.len()); + for (index, name) in names.iter().enumerate().take(tokens.len()) { + let context = TokenContext { + raws, + uppers: &uppers, + index, + }; + foldable.push(!name && context.is_foldable(dialect.dialect_type())); + } + foldable +} diff --git a/crates/sqlclean/src/sql_tokens/main/is_unquoted_word.rs b/crates/sqlclean/src/sql_tokens/main/is_unquoted_word.rs new file mode 100644 index 0000000..6ea92ef --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/is_unquoted_word.rs @@ -0,0 +1,11 @@ +//! Unquoted SQL word recognition shared by canonical tokens and the formatter. + +/// Return whether `text` is an unquoted ASCII word whose letter case carries no meaning. +pub fn is_unquoted_word(text: &str) -> bool { + let mut characters = text.chars(); + characters + .next() + .is_some_and(|first| first.is_ascii_alphabetic() || first == '_') + && characters + .all(|character| character.is_ascii_alphanumeric() || matches!(character, '_' | '$')) +} diff --git a/crates/sqlclean/src/sql_tokens/main/mod.rs b/crates/sqlclean/src/sql_tokens/main/mod.rs new file mode 100644 index 0000000..02cb65b --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/mod.rs @@ -0,0 +1,10 @@ +#[cfg(any(test, feature = "test-support"))] +pub mod builtin_function_names; +pub mod canonical_tokens; +pub mod case_folding; +pub mod is_unquoted_word; +pub mod name_positions; +pub mod projection_spans; +#[cfg(any(test, feature = "test-support"))] +pub mod query_fingerprint; +pub mod token_texts; diff --git a/crates/sqlclean/src/sql_tokens/main/name_positions.rs b/crates/sqlclean/src/sql_tokens/main/name_positions.rs new file mode 100644 index 0000000..b056a9b --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/name_positions.rs @@ -0,0 +1,16 @@ +//! Token positions where some dialect accepts even a reserved word as an unquoted name. + +use crate::sql_tokens::constants::{ALIAS_KEYWORD, NAME_PREFIXES}; + +/// Return, for every upper-cased token, whether it follows `.`, `:`, `@`, `$` or `AS`. +pub fn name_positions(uppers: &[String]) -> Vec { + let mut positions = Vec::with_capacity(uppers.len()); + let mut previous: Option<&str> = None; + for upper in uppers { + positions.push( + previous.is_some_and(|word| word == ALIAS_KEYWORD || NAME_PREFIXES.contains(&word)), + ); + previous = Some(upper.as_str()); + } + positions +} diff --git a/crates/sqlclean/src/sql_tokens/main/projection_spans.rs b/crates/sqlclean/src/sql_tokens/main/projection_spans.rs new file mode 100644 index 0000000..5965bd7 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/projection_spans.rs @@ -0,0 +1,41 @@ +//! Authored spans of the top-level items of one select list. + +use polyglot_sql::tokens::{Token, TokenType}; + +use crate::sql_tokens::constants::PROJECTION_ENDS; + +/// Spans of each top-level projection item of the select starting at token `select`. +pub fn projection_spans(tokens: &[Token], select: usize) -> Vec<(usize, usize)> { + let mut spans: Vec<(usize, usize)> = Vec::new(); + let mut depth: usize = 0; + let mut start: Option = None; + let mut end: usize = 0; + for token in tokens.iter().skip(select + 1) { + if matches!( + token.token_type, + TokenType::Space | TokenType::Break | TokenType::LineComment | TokenType::BlockComment + ) { + continue; + } + match token.token_type { + TokenType::LParen => depth += 1, + TokenType::RParen if depth == 0 => break, + TokenType::RParen => depth -= 1, + TokenType::Comma if depth == 0 => { + if let Some(first) = start.take() { + spans.push((first, end)); + } + continue; + } + kind if depth == 0 && PROJECTION_ENDS.contains(&kind) => break, + TokenType::Distinct | TokenType::All if depth == 0 && start.is_none() => continue, + _ => {} + } + start.get_or_insert(token.span.start); + end = token.span.end; + } + if let Some(first) = start { + spans.push((first, end)); + } + spans +} diff --git a/crates/sqlclean/src/sql_tokens/main/query_fingerprint.rs b/crates/sqlclean/src/sql_tokens/main/query_fingerprint.rs new file mode 100644 index 0000000..4cf7751 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/query_fingerprint.rs @@ -0,0 +1,25 @@ +//! Whitespace-, comment- and keyword-case-insensitive SQL change fingerprints. + +use std::str::FromStr; + +use polyglot_sql::{Dialect, DialectType}; +use sha2::{Digest, Sha256}; + +use crate::sql_tokens::main::canonical_tokens::canonical_tokens; + +/// Return the SHA-256 hex digest of the canonical token stream of `sql` under `dialect_name`. +pub fn query_fingerprint(sql: &str, dialect_name: &str) -> Result { + let dialect_type = DialectType::from_str(dialect_name) + .map_err(|_| format!("unknown SQL fingerprint dialect '{dialect_name}'"))?; + let dialect = Dialect::get(dialect_type); + let mut digest = Sha256::new(); + let tokens = dialect + .tokenize(sql) + .map_err(|error| format!("SQL tokenization failed: {error}"))?; + for token in canonical_tokens(sql, &tokens, &dialect)? { + digest.update(token.text.len().to_string().as_bytes()); + digest.update(b":"); + digest.update(token.text.as_bytes()); + } + Ok(format!("{:x}", digest.finalize())) +} diff --git a/crates/sqlclean/src/sql_tokens/main/token_texts.rs b/crates/sqlclean/src/sql_tokens/main/token_texts.rs new file mode 100644 index 0000000..8467369 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/main/token_texts.rs @@ -0,0 +1,16 @@ +//! Authored text of tokens, taken from their character spans. + +use polyglot_sql::tokens::Token; + +/// Return the authored text of every token in `sql`. +pub fn token_texts(sql: &str, tokens: &[Token]) -> Result, String> { + let characters: Vec = sql.chars().collect(); + let mut texts = Vec::with_capacity(tokens.len()); + for token in tokens { + let slice = characters + .get(token.span.start..token.span.end) + .ok_or_else(|| "SQL tokenizer returned an invalid token span".to_string())?; + texts.push(slice.iter().collect()); + } + Ok(texts) +} diff --git a/crates/sqlclean/src/sql_tokens/mod.rs b/crates/sqlclean/src/sql_tokens/mod.rs new file mode 100644 index 0000000..6b100c6 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/mod.rs @@ -0,0 +1,9 @@ +//! The canonical SQL token stream: authored tokens without layout or comments. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean/src/sql_tokens/models.rs b/crates/sqlclean/src/sql_tokens/models.rs new file mode 100644 index 0000000..b18ebb1 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/models.rs @@ -0,0 +1,7 @@ +//! Canonical token values shared by the formatter invariant and change fingerprints. + +/// One authored token with layout removed: keywords and built-in function names upper-cased. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CanonicalToken { + pub text: String, +} diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/bigquery.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/bigquery.txt new file mode 100644 index 0000000..ded970a --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/bigquery.txt @@ -0,0 +1,99 @@ +# BigQuery (GoogleSQL) reserved keywords: 'Reserved keywords' in +# https://cloud.google.com/bigquery/docs/reference/standard-sql/lexical +all +and +any +array +as +asc +assert_rows_modified +at +between +by +case +cast +collate +contains +create +cross +cube +current +default +define +desc +distinct +else +end +enum +escape +except +exclude +exists +extract +false +fetch +following +for +from +full +graph_table +group +grouping +groups +hash +having +if +ignore +in +inner +intersect +interval +into +is +join +lateral +left +like +limit +lookup +merge +natural +new +no +not +null +nulls +of +on +or +order +outer +over +partition +preceding +proto +qualify +range +recursive +respect +right +rollup +rows +select +set +some +struct +tablesample +then +to +treat +true +unbounded +union +unnest +using +when +where +window +with +within diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/databricks.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/databricks.txt new file mode 100644 index 0000000..dc43281 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/databricks.txt @@ -0,0 +1,4 @@ +# Databricks reserves no keywords as identifiers: +# https://docs.databricks.com/sql/language-manual/sql-ref-reserved-words +# ('Databricks does not formally disallow any specific literals from being used as identifiers'). + diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/duckdb.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/duckdb.txt new file mode 100644 index 0000000..4374c4e --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/duckdb.txt @@ -0,0 +1,77 @@ +# DuckDB reserved keywords: SELECT keyword_name FROM duckdb_keywords() +# WHERE keyword_category = 'reserved' (DuckDB 1.5.2). +all +analyse +analyze +and +any +array +as +asc +asymmetric +both +case +cast +check +collate +column +constraint +create +default +deferrable +desc +describe +distinct +do +else +end +except +false +fetch +for +foreign +from +group +having +in +initially +intersect +into +lambda +lateral +leading +limit +not +null +offset +on +only +or +order +pivot +pivot_longer +pivot_wider +placing +primary +qualify +references +returning +select +show +some +summarize +symmetric +table +then +to +trailing +true +union +unique +unpivot +using +variadic +when +where +window +with diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/generic.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/generic.txt new file mode 100644 index 0000000..05f8ee4 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/generic.txt @@ -0,0 +1,28 @@ +# Generic dialect: keywords reserved in every dialect file above except Databricks +# (DuckDB, PostgreSQL, BigQuery, Snowflake and T-SQL). +all +and +any +as +create +distinct +else +for +from +group +having +in +intersect +into +not +null +on +or +order +select +some +then +to +union +where +with diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/postgres.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/postgres.txt new file mode 100644 index 0000000..7a57911 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/postgres.txt @@ -0,0 +1,80 @@ +# PostgreSQL reserved keywords (non-function): RESERVED_KEYWORD entries of +# src/include/parser/kwlist.h (REL_17_STABLE); Appendix C 'reserved'. +all +analyse +analyze +and +any +array +as +asc +asymmetric +both +case +cast +check +collate +column +constraint +create +current_catalog +current_date +current_role +current_time +current_timestamp +current_user +default +deferrable +desc +distinct +do +else +end +except +false +fetch +for +foreign +from +grant +group +having +in +initially +intersect +into +lateral +leading +limit +localtime +localtimestamp +not +null +offset +on +only +or +order +placing +primary +references +returning +select +session_user +some +symmetric +system_user +table +then +to +trailing +true +union +unique +user +using +variadic +when +where +window +with diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/snowflake.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/snowflake.txt new file mode 100644 index 0000000..511b646 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/snowflake.txt @@ -0,0 +1,65 @@ +# Snowflake reserved keywords: entries marked 'Reserved by ANSI', 'Reserved by Snowflake' +# or 'Reserved by Snowflake and others' in https://docs.snowflake.com/en/sql-reference/reserved-keywords. +# Limited keywords (usable as identifiers in some positions) are excluded. +all +alter +and +any +as +between +by +check +column +connect +create +current +delete +distinct +drop +else +exists +following +for +from +grant +group +having +ilike +in +increment +insert +intersect +into +is +like +minus +not +null +of +on +or +order +qualify +regexp +revoke +rlike +row +rows +sample +select +set +some +start +table +tablesample +then +to +trigger +union +unique +update +values +whenever +where +window +with diff --git a/crates/sqlclean/src/sql_tokens/reserved_keywords/tsql.txt b/crates/sqlclean/src/sql_tokens/reserved_keywords/tsql.txt new file mode 100644 index 0000000..b83a35b --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/reserved_keywords/tsql.txt @@ -0,0 +1,186 @@ +# T-SQL reserved keywords (SQL Server and Azure Synapse table) in +# https://learn.microsoft.com/sql/t-sql/language-elements/reserved-keywords-transact-sql +add +all +alter +and +any +as +asc +authorization +backup +begin +between +break +browse +bulk +by +cascade +case +check +checkpoint +close +clustered +coalesce +collate +column +commit +compute +constraint +contains +containstable +continue +convert +create +cross +current +current_date +current_time +current_timestamp +current_user +cursor +database +dbcc +deallocate +declare +default +delete +deny +desc +disk +distinct +distributed +double +drop +dump +else +end +errlvl +escape +except +exec +execute +exists +exit +external +fetch +file +fillfactor +for +foreign +freetext +freetexttable +from +full +function +goto +grant +group +having +holdlock +identity +identity_insert +identitycol +if +in +index +inner +insert +intersect +into +is +join +key +kill +left +like +lineno +load +merge +national +nocheck +nonclustered +not +null +nullif +of +off +offsets +on +open +opendatasource +openquery +openrowset +openxml +option +or +order +outer +over +percent +pivot +plan +precision +primary +print +proc +procedure +public +raiserror +read +readtext +reconfigure +references +replication +restore +restrict +return +revert +revoke +right +rollback +rowcount +rowguidcol +rule +save +schema +securityaudit +select +semantickeyphrasetable +semanticsimilaritydetailstable +semanticsimilaritytable +session_user +set +setuser +shutdown +some +statistics +system_user +table +tablesample +textsize +then +to +top +tran +transaction +trigger +truncate +try_convert +tsequal +union +unique +unpivot +update +updatetext +use +user +values +varying +view +waitfor +when +where +while +with +writetext diff --git a/crates/sqlclean/src/sql_tokens/tests.rs b/crates/sqlclean/src/sql_tokens/tests.rs new file mode 100644 index 0000000..5468fc5 --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_canonical_tokens.rs"] +mod canonical_tokens; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean/src/sql_tokens/tests/test_canonical_tokens.rs b/crates/sqlclean/src/sql_tokens/tests/test_canonical_tokens.rs new file mode 100644 index 0000000..2dec3bf --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/tests/test_canonical_tokens.rs @@ -0,0 +1,218 @@ +use polyglot_sql::{Dialect, DialectType}; + +use crate::sql_tokens::main::canonical_tokens::canonical_tokens; +use crate::sql_tokens::tests::test_types::CanonicalTokensTestCase; + +#[test] +fn given_sql_pairs_when_canonicalizing_then_only_layout_comments_and_word_case_are_ignored() +-> Result<(), String> { + let test_cases = [ + CanonicalTokensTestCase { + description: "whitespace between tokens is ignored", + dialect: DialectType::DuckDB, + left: "select a,b from orders", + right: "SELECT\n a, b\nFROM orders", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "comments are ignored", + dialect: DialectType::Snowflake, + left: "select a -- note\nfrom orders /* block */", + right: "select a from orders", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "keyword and built-in function case is ignored", + dialect: DialectType::Snowflake, + left: "select count(*) from orders", + right: "SELECT COUNT(*) FROM orders", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "unquoted identifier case is significant", + dialect: DialectType::Snowflake, + left: "select count(*) from orders", + right: "SELECT COUNT(*) FROM ORDERS", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "Snowflake path key case is significant", + dialect: DialectType::Snowflake, + left: "select payload:customerId from t", + right: "select payload:customerid from t", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "BigQuery qualified table case is significant", + dialect: DialectType::BigQuery, + left: "select * from ds.Orders", + right: "select * from ds.orders", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "DuckDB alias case is significant", + dialect: DialectType::DuckDB, + left: "select 1 as Foo", + right: "select 1 as foo", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "keyword-named qualified name case is significant", + dialect: DialectType::BigQuery, + left: "select * from inventory.view", + right: "select * from inventory.VIEW", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "keyword-named alias case is significant", + dialect: DialectType::DuckDB, + left: "select pos as index from t", + right: "select pos AS INDEX from t", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "user-defined function case is significant", + dialect: DialectType::DuckDB, + left: "select my_udf(a) from t", + right: "select MY_UDF(a) from t", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "built-in function case is ignored", + dialect: DialectType::DuckDB, + left: "select coalesce(a, 0) from t", + right: "select COALESCE(a, 0) from t", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "Snowflake double-slash comments are ignored", + dialect: DialectType::Snowflake, + left: "select a // note\nfrom t", + right: "select a from t", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "BigQuery hash comments are ignored", + dialect: DialectType::BigQuery, + left: "select a # note\nfrom t", + right: "select a from t", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "DuckDB struct-literal key case is significant", + dialect: DialectType::DuckDB, + left: "select {Key: 1}", + right: "select {key: 1}", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "a non-reserved keyword operand before an operator keeps its case", + dialect: DialectType::DuckDB, + left: "select a, Rows ^ 2 from t", + right: "select a, rows ^ 2 from t", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "a non-reserved keyword table in a comma join keeps its case", + dialect: DialectType::BigQuery, + left: "select * from ds.a, View v", + right: "select * from ds.a, view v", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "a reserved word after a dot is a field name", + dialect: DialectType::BigQuery, + left: "select abc.Group from abc", + right: "select abc.group from abc", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "a reserved keyword column alias keeps its case", + dialect: DialectType::DuckDB, + left: "select 1 as Select", + right: "select 1 as select", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "reserved keyword case is ignored", + dialect: DialectType::DuckDB, + left: "select a from t where a in (1) limit 1", + right: "SELECT a FROM t WHERE a IN (1) LIMIT 1", + expected_equal: true, + }, + CanonicalTokensTestCase { + description: "Databricks reserves no keywords", + dialect: DialectType::Databricks, + left: "select a from t", + right: "SELECT a FROM t", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "whitespace inside a string literal is significant", + dialect: DialectType::DuckDB, + left: "select 'a b' as code", + right: "select 'a b' as code", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "string literal case is significant", + dialect: DialectType::DuckDB, + left: "select 'Open' as status", + right: "select 'OPEN' as status", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "quoted identifier case is significant", + dialect: DialectType::Snowflake, + left: "select \"Order\" from orders", + right: "select \"ORDER\" from orders", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "dollar-quoted literal content is significant", + dialect: DialectType::PostgreSQL, + left: "select $$a b$$ as note", + right: "select $$a b$$ as note", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "number spelling is significant", + dialect: DialectType::DuckDB, + left: "select 1.0 as amount", + right: "select 1.00 as amount", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "a renamed function is significant", + dialect: DialectType::Snowflake, + left: "select startswith(code, 'EU')", + right: "select starts_with(code, 'EU')", + expected_equal: false, + }, + CanonicalTokensTestCase { + description: "an added alias keyword is significant", + dialect: DialectType::DuckDB, + left: "select a b from orders", + right: "select a as b from orders", + expected_equal: false, + }, + ]; + for test_case in test_cases { + let dialect = Dialect::get(test_case.dialect); + let left_tokens = dialect + .tokenize(test_case.left) + .map_err(|error| error.to_string())?; + let right_tokens = dialect + .tokenize(test_case.right) + .map_err(|error| error.to_string())?; + let left = canonical_tokens(test_case.left, &left_tokens, &dialect)?; + let right = canonical_tokens(test_case.right, &right_tokens, &dialect)?; + assert_eq!( + left == right, + test_case.expected_equal, + "{}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean/src/sql_tokens/tests/test_types.rs b/crates/sqlclean/src/sql_tokens/tests/test_types.rs new file mode 100644 index 0000000..2e7671d --- /dev/null +++ b/crates/sqlclean/src/sql_tokens/tests/test_types.rs @@ -0,0 +1,7 @@ +pub(crate) struct CanonicalTokensTestCase { + pub(crate) description: &'static str, + pub(crate) dialect: polyglot_sql::DialectType, + pub(crate) left: &'static str, + pub(crate) right: &'static str, + pub(crate) expected_equal: bool, +} From 1dd5c7dc4b384159efc3f0af32e60651ba0fe465 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:26 +0000 Subject: [PATCH 03/29] feat: add the sqlclean check, fix, format and rules commands Configuration comes from the nearest sqlclean.toml. Results are cached per file, fixes run until stable and unsafe fixes need --unsafe-fixes. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .pre-commit-hooks.yaml | 23 ++ crates/sqlclean-cli/Cargo.toml | 31 +++ .../src/caching/_helpers/entries.rs | 13 ++ .../sqlclean-cli/src/caching/_helpers/mod.rs | 1 + crates/sqlclean-cli/src/caching/constants.rs | 4 + .../src/caching/main/cache_key.rs | 14 ++ crates/sqlclean-cli/src/caching/main/mod.rs | 1 + crates/sqlclean-cli/src/caching/mod.rs | 9 + crates/sqlclean-cli/src/caching/models.rs | 105 ++++++++++ crates/sqlclean-cli/src/caching/tests.rs | 4 + .../src/caching/tests/test_result_cache.rs | 128 +++++++++++ .../src/caching/tests/test_types.rs | 17 ++ .../src/checking/_helpers/builtin.rs | 88 ++++++++ .../src/checking/_helpers/catalogue.rs | 109 ++++++++++ .../src/checking/_helpers/custom.rs | 198 ++++++++++++++++++ .../src/checking/_helpers/exemptions.rs | 97 +++++++++ .../sqlclean-cli/src/checking/_helpers/mod.rs | 6 + .../src/checking/_helpers/orchestration.rs | 179 ++++++++++++++++ .../src/checking/_helpers/selection.rs | 39 ++++ crates/sqlclean-cli/src/checking/constants.rs | 15 ++ .../src/checking/main/check_sources.rs | 14 ++ crates/sqlclean-cli/src/checking/main/mod.rs | 2 + .../src/checking/main/rule_catalogue.rs | 18 ++ crates/sqlclean-cli/src/checking/mod.rs | 6 + crates/sqlclean-cli/src/checking/models.rs | 77 +++++++ .../src/command/_helpers/check_command.rs | 33 +++ .../src/command/_helpers/diffs.rs | 25 +++ .../src/command/_helpers/dispatch.rs | 55 +++++ .../src/command/_helpers/facts_command.rs | 19 ++ .../src/command/_helpers/fix_command.rs | 70 +++++++ .../src/command/_helpers/format_command.rs | 49 +++++ .../sqlclean-cli/src/command/_helpers/mod.rs | 7 + .../src/command/_helpers/rules_command.rs | 77 +++++++ crates/sqlclean-cli/src/command/constants.rs | 5 + crates/sqlclean-cli/src/command/main/mod.rs | 1 + crates/sqlclean-cli/src/command/main/run.rs | 10 + crates/sqlclean-cli/src/command/mod.rs | 6 + crates/sqlclean-cli/src/command/models.rs | 94 +++++++++ .../src/config/_helpers/file_schema.rs | 46 ++++ .../src/config/_helpers/loading.rs | 64 ++++++ .../sqlclean-cli/src/config/_helpers/mod.rs | 3 + .../src/config/_helpers/validation.rs | 56 +++++ crates/sqlclean-cli/src/config/constants.rs | 6 + .../src/config/main/load_config.rs | 10 + crates/sqlclean-cli/src/config/main/mod.rs | 1 + crates/sqlclean-cli/src/config/mod.rs | 9 + crates/sqlclean-cli/src/config/models.rs | 80 +++++++ crates/sqlclean-cli/src/config/tests.rs | 4 + .../src/config/tests/test_load_config.rs | 118 +++++++++++ .../src/config/tests/test_types.rs | 16 ++ .../src/discovery/_helpers/matching.rs | 60 ++++++ .../src/discovery/_helpers/mod.rs | 2 + .../src/discovery/_helpers/walking.rs | 77 +++++++ .../src/discovery/main/discover_files.rs | 11 + crates/sqlclean-cli/src/discovery/main/mod.rs | 2 + .../src/discovery/main/path_patterns.rs | 8 + crates/sqlclean-cli/src/discovery/mod.rs | 8 + crates/sqlclean-cli/src/discovery/models.rs | 11 + crates/sqlclean-cli/src/discovery/tests.rs | 4 + .../discovery/tests/test_discover_files.rs | 67 ++++++ .../src/discovery/tests/test_types.rs | 5 + .../src/fixing/_helpers/edit_selection.rs | 56 +++++ .../sqlclean-cli/src/fixing/_helpers/mod.rs | 2 + .../src/fixing/_helpers/passes.rs | 74 +++++++ crates/sqlclean-cli/src/fixing/constants.rs | 2 + .../src/fixing/main/fix_sources.rs | 16 ++ crates/sqlclean-cli/src/fixing/main/mod.rs | 1 + crates/sqlclean-cli/src/fixing/mod.rs | 9 + crates/sqlclean-cli/src/fixing/models.rs | 14 ++ crates/sqlclean-cli/src/fixing/tests.rs | 6 + .../sqlclean-cli/src/fixing/tests/helpers.rs | 27 +++ .../src/fixing/tests/test_edit_selection.rs | 37 ++++ .../src/fixing/tests/test_types.rs | 5 + .../sqlclean-cli/src/formatting/constants.rs | 2 + .../src/formatting/main/format_source.rs | 54 +++++ .../sqlclean-cli/src/formatting/main/mod.rs | 1 + crates/sqlclean-cli/src/formatting/mod.rs | 4 + .../src/hosting/_helpers/interpreter.rs | 26 +++ .../sqlclean-cli/src/hosting/_helpers/mod.rs | 2 + .../src/hosting/_helpers/process.rs | 57 +++++ crates/sqlclean-cli/src/hosting/constants.rs | 7 + crates/sqlclean-cli/src/hosting/main/mod.rs | 1 + .../sqlclean-cli/src/hosting/main/run_host.rs | 8 + crates/sqlclean-cli/src/hosting/mod.rs | 6 + crates/sqlclean-cli/src/hosting/models.rs | 59 ++++++ crates/sqlclean-cli/src/lib.rs | 14 ++ crates/sqlclean-cli/src/main.rs | 9 + .../src/positions/main/apply_edits.rs | 18 ++ crates/sqlclean-cli/src/positions/main/mod.rs | 1 + crates/sqlclean-cli/src/positions/mod.rs | 7 + crates/sqlclean-cli/src/positions/models.rs | 37 ++++ crates/sqlclean-cli/src/positions/tests.rs | 4 + .../src/positions/tests/test_positions.rs | 78 +++++++ .../src/positions/tests/test_types.rs | 12 ++ .../src/reporting/_helpers/location.rs | 32 +++ .../src/reporting/_helpers/machine.rs | 68 ++++++ .../src/reporting/_helpers/mod.rs | 3 + .../src/reporting/_helpers/text.rs | 90 ++++++++ crates/sqlclean-cli/src/reporting/main/mod.rs | 1 + .../src/reporting/main/render_findings.rs | 20 ++ crates/sqlclean-cli/src/reporting/mod.rs | 5 + crates/sqlclean-cli/src/reporting/models.rs | 24 +++ 102 files changed, 3216 insertions(+) create mode 100644 .pre-commit-hooks.yaml create mode 100644 crates/sqlclean-cli/Cargo.toml create mode 100644 crates/sqlclean-cli/src/caching/_helpers/entries.rs create mode 100644 crates/sqlclean-cli/src/caching/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/caching/constants.rs create mode 100644 crates/sqlclean-cli/src/caching/main/cache_key.rs create mode 100644 crates/sqlclean-cli/src/caching/main/mod.rs create mode 100644 crates/sqlclean-cli/src/caching/mod.rs create mode 100644 crates/sqlclean-cli/src/caching/models.rs create mode 100644 crates/sqlclean-cli/src/caching/tests.rs create mode 100644 crates/sqlclean-cli/src/caching/tests/test_result_cache.rs create mode 100644 crates/sqlclean-cli/src/caching/tests/test_types.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/builtin.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/catalogue.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/custom.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/exemptions.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/orchestration.rs create mode 100644 crates/sqlclean-cli/src/checking/_helpers/selection.rs create mode 100644 crates/sqlclean-cli/src/checking/constants.rs create mode 100644 crates/sqlclean-cli/src/checking/main/check_sources.rs create mode 100644 crates/sqlclean-cli/src/checking/main/mod.rs create mode 100644 crates/sqlclean-cli/src/checking/main/rule_catalogue.rs create mode 100644 crates/sqlclean-cli/src/checking/mod.rs create mode 100644 crates/sqlclean-cli/src/checking/models.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/check_command.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/diffs.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/dispatch.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/facts_command.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/fix_command.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/format_command.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/command/_helpers/rules_command.rs create mode 100644 crates/sqlclean-cli/src/command/constants.rs create mode 100644 crates/sqlclean-cli/src/command/main/mod.rs create mode 100644 crates/sqlclean-cli/src/command/main/run.rs create mode 100644 crates/sqlclean-cli/src/command/mod.rs create mode 100644 crates/sqlclean-cli/src/command/models.rs create mode 100644 crates/sqlclean-cli/src/config/_helpers/file_schema.rs create mode 100644 crates/sqlclean-cli/src/config/_helpers/loading.rs create mode 100644 crates/sqlclean-cli/src/config/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/config/_helpers/validation.rs create mode 100644 crates/sqlclean-cli/src/config/constants.rs create mode 100644 crates/sqlclean-cli/src/config/main/load_config.rs create mode 100644 crates/sqlclean-cli/src/config/main/mod.rs create mode 100644 crates/sqlclean-cli/src/config/mod.rs create mode 100644 crates/sqlclean-cli/src/config/models.rs create mode 100644 crates/sqlclean-cli/src/config/tests.rs create mode 100644 crates/sqlclean-cli/src/config/tests/test_load_config.rs create mode 100644 crates/sqlclean-cli/src/config/tests/test_types.rs create mode 100644 crates/sqlclean-cli/src/discovery/_helpers/matching.rs create mode 100644 crates/sqlclean-cli/src/discovery/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/discovery/_helpers/walking.rs create mode 100644 crates/sqlclean-cli/src/discovery/main/discover_files.rs create mode 100644 crates/sqlclean-cli/src/discovery/main/mod.rs create mode 100644 crates/sqlclean-cli/src/discovery/main/path_patterns.rs create mode 100644 crates/sqlclean-cli/src/discovery/mod.rs create mode 100644 crates/sqlclean-cli/src/discovery/models.rs create mode 100644 crates/sqlclean-cli/src/discovery/tests.rs create mode 100644 crates/sqlclean-cli/src/discovery/tests/test_discover_files.rs create mode 100644 crates/sqlclean-cli/src/discovery/tests/test_types.rs create mode 100644 crates/sqlclean-cli/src/fixing/_helpers/edit_selection.rs create mode 100644 crates/sqlclean-cli/src/fixing/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/fixing/_helpers/passes.rs create mode 100644 crates/sqlclean-cli/src/fixing/constants.rs create mode 100644 crates/sqlclean-cli/src/fixing/main/fix_sources.rs create mode 100644 crates/sqlclean-cli/src/fixing/main/mod.rs create mode 100644 crates/sqlclean-cli/src/fixing/mod.rs create mode 100644 crates/sqlclean-cli/src/fixing/models.rs create mode 100644 crates/sqlclean-cli/src/fixing/tests.rs create mode 100644 crates/sqlclean-cli/src/fixing/tests/helpers.rs create mode 100644 crates/sqlclean-cli/src/fixing/tests/test_edit_selection.rs create mode 100644 crates/sqlclean-cli/src/fixing/tests/test_types.rs create mode 100644 crates/sqlclean-cli/src/formatting/constants.rs create mode 100644 crates/sqlclean-cli/src/formatting/main/format_source.rs create mode 100644 crates/sqlclean-cli/src/formatting/main/mod.rs create mode 100644 crates/sqlclean-cli/src/formatting/mod.rs create mode 100644 crates/sqlclean-cli/src/hosting/_helpers/interpreter.rs create mode 100644 crates/sqlclean-cli/src/hosting/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/hosting/_helpers/process.rs create mode 100644 crates/sqlclean-cli/src/hosting/constants.rs create mode 100644 crates/sqlclean-cli/src/hosting/main/mod.rs create mode 100644 crates/sqlclean-cli/src/hosting/main/run_host.rs create mode 100644 crates/sqlclean-cli/src/hosting/mod.rs create mode 100644 crates/sqlclean-cli/src/hosting/models.rs create mode 100644 crates/sqlclean-cli/src/lib.rs create mode 100644 crates/sqlclean-cli/src/main.rs create mode 100644 crates/sqlclean-cli/src/positions/main/apply_edits.rs create mode 100644 crates/sqlclean-cli/src/positions/main/mod.rs create mode 100644 crates/sqlclean-cli/src/positions/mod.rs create mode 100644 crates/sqlclean-cli/src/positions/models.rs create mode 100644 crates/sqlclean-cli/src/positions/tests.rs create mode 100644 crates/sqlclean-cli/src/positions/tests/test_positions.rs create mode 100644 crates/sqlclean-cli/src/positions/tests/test_types.rs create mode 100644 crates/sqlclean-cli/src/reporting/_helpers/location.rs create mode 100644 crates/sqlclean-cli/src/reporting/_helpers/machine.rs create mode 100644 crates/sqlclean-cli/src/reporting/_helpers/mod.rs create mode 100644 crates/sqlclean-cli/src/reporting/_helpers/text.rs create mode 100644 crates/sqlclean-cli/src/reporting/main/mod.rs create mode 100644 crates/sqlclean-cli/src/reporting/main/render_findings.rs create mode 100644 crates/sqlclean-cli/src/reporting/mod.rs create mode 100644 crates/sqlclean-cli/src/reporting/models.rs diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..b284a28 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,23 @@ +- id: sqlclean-check + name: sqlclean check + description: Report SQL lint findings without changing files. + entry: sqlclean check + language: python + types: [sql] + require_serial: true + +- id: sqlclean-fix + name: sqlclean fix + description: Apply safe SQL lint fixes, then format. + entry: sqlclean fix + language: python + types: [sql] + require_serial: true + +- id: sqlclean-format + name: sqlclean format + description: Rewrite SQL files in the canonical layout. + entry: sqlclean format + language: python + types: [sql] + require_serial: true diff --git a/crates/sqlclean-cli/Cargo.toml b/crates/sqlclean-cli/Cargo.toml new file mode 100644 index 0000000..60386b4 --- /dev/null +++ b/crates/sqlclean-cli/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "sqlclean-cli" +version.workspace = true +description = "The sqlclean command: check, fix and format SQL files." +edition.workspace = true +license.workspace = true +publish.workspace = true +repository.workspace = true +rust-version.workspace = true + +[[bin]] +name = "sqlclean" +path = "src/main.rs" + +[dependencies] +clap.workspace = true +globset.workspace = true +ignore.workspace = true +rayon.workspace = true +serde.workspace = true +serde_json.workspace = true +sha2.workspace = true +similar.workspace = true +sqlclean.workspace = true +toml.workspace = true + +[dev-dependencies] +tempfile.workspace = true + +[lints] +workspace = true diff --git a/crates/sqlclean-cli/src/caching/_helpers/entries.rs b/crates/sqlclean-cli/src/caching/_helpers/entries.rs new file mode 100644 index 0000000..58d502a --- /dev/null +++ b/crates/sqlclean-cli/src/caching/_helpers/entries.rs @@ -0,0 +1,13 @@ +//! Read the stored cache file. + +use std::collections::BTreeMap; +use std::fs; +use std::path::Path; + +use crate::caching::models::CacheEntry; + +/// The entries stored in `file`; a missing or damaged file is an error the caller may drop. +pub(crate) fn read_entries(file: &Path) -> Result, String> { + let bytes: Vec = fs::read(file).map_err(|error| error.to_string())?; + serde_json::from_slice(&bytes).map_err(|error| error.to_string()) +} diff --git a/crates/sqlclean-cli/src/caching/_helpers/mod.rs b/crates/sqlclean-cli/src/caching/_helpers/mod.rs new file mode 100644 index 0000000..f700be7 --- /dev/null +++ b/crates/sqlclean-cli/src/caching/_helpers/mod.rs @@ -0,0 +1 @@ +pub mod entries; diff --git a/crates/sqlclean-cli/src/caching/constants.rs b/crates/sqlclean-cli/src/caching/constants.rs new file mode 100644 index 0000000..e750637 --- /dev/null +++ b/crates/sqlclean-cli/src/caching/constants.rs @@ -0,0 +1,4 @@ +pub(crate) const CACHE_DIRECTORY: &str = ".sqlclean/cache"; +pub(crate) const CACHE_FILE: &str = "results-v1.json"; +pub(crate) const CACHE_PARENT_IGNORE: &str = ".sqlclean/.gitignore"; +pub(crate) const CACHE_PARENT_IGNORE_TEXT: &str = "*\n"; diff --git a/crates/sqlclean-cli/src/caching/main/cache_key.rs b/crates/sqlclean-cli/src/caching/main/cache_key.rs new file mode 100644 index 0000000..ed2af4a --- /dev/null +++ b/crates/sqlclean-cli/src/caching/main/cache_key.rs @@ -0,0 +1,14 @@ +//! The digest that identifies one cached result. + +use sha2::{Digest, Sha256}; + +/// A SHA-256 over this build's version and every part, each prefixed by its length. +pub fn cache_key(parts: &[&str]) -> String { + let mut digest = Sha256::new(); + for part in [env!("CARGO_PKG_VERSION")].iter().chain(parts) { + digest.update(part.len().to_string().as_bytes()); + digest.update(b":"); + digest.update(part.as_bytes()); + } + format!("{:x}", digest.finalize()) +} diff --git a/crates/sqlclean-cli/src/caching/main/mod.rs b/crates/sqlclean-cli/src/caching/main/mod.rs new file mode 100644 index 0000000..b92fc68 --- /dev/null +++ b/crates/sqlclean-cli/src/caching/main/mod.rs @@ -0,0 +1 @@ +pub mod cache_key; diff --git a/crates/sqlclean-cli/src/caching/mod.rs b/crates/sqlclean-cli/src/caching/mod.rs new file mode 100644 index 0000000..ae7f4ca --- /dev/null +++ b/crates/sqlclean-cli/src/caching/mod.rs @@ -0,0 +1,9 @@ +//! Findings reused while their inputs are unchanged, in `.sqlclean/cache` under the root. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean-cli/src/caching/models.rs b/crates/sqlclean-cli/src/caching/models.rs new file mode 100644 index 0000000..018b2de --- /dev/null +++ b/crates/sqlclean-cli/src/caching/models.rs @@ -0,0 +1,105 @@ +//! The result cache of one command run. + +use std::collections::{BTreeMap, HashSet}; +use std::fs; +use std::path::{Path, PathBuf}; + +use serde::de::DeserializeOwned; +use serde::{Deserialize, Serialize}; + +use crate::caching::_helpers::entries::read_entries; +use crate::caching::constants::{ + CACHE_DIRECTORY, CACHE_FILE, CACHE_PARENT_IGNORE, CACHE_PARENT_IGNORE_TEXT, +}; + +/// One cached value and the file it describes, `None` for repository-wide results. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CacheEntry { + pub path: Option, + pub value: serde_json::Value, +} + +/// Cached results by key. A damaged or unreadable cache file is treated as empty. +#[derive(Debug, Default)] +pub struct ResultCache { + file: Option, + root: PathBuf, + entries: BTreeMap, + fresh: BTreeMap, +} + +impl ResultCache { + /// The cache under `root`, or a cache that stores nothing when `enabled` is false. + pub fn open(root: &Path, enabled: bool) -> Self { + if !enabled { + return Self::default(); + } + let file: PathBuf = root.join(CACHE_DIRECTORY).join(CACHE_FILE); + let entries: BTreeMap = read_entries(&file).unwrap_or_default(); + Self { + file: Some(file), + root: root.to_path_buf(), + entries, + fresh: BTreeMap::new(), + } + } + + pub fn get(&mut self, key: &str) -> Option { + self.file.as_ref()?; + let entry: CacheEntry = self.entries.get(key)?.clone(); + match serde_json::from_value(entry.value.clone()) { + Ok(value) => { + self.fresh.insert(key.to_owned(), entry); + Some(value) + } + Err(_) => None, + } + } + + pub fn put(&mut self, key: &str, path: Option<&str>, value: &T) { + if self.file.is_none() { + return; + } + let value: serde_json::Value = match serde_json::to_value(value) { + Ok(value) => value, + Err(_) => return, + }; + self.fresh.insert( + key.to_owned(), + CacheEntry { + path: path.map(ToOwned::to_owned), + value, + }, + ); + } + + /// Keep this run's entries and earlier entries for files this run did not read. + pub fn save(self, read_paths: &HashSet) -> Result<(), String> { + let Some(file) = self.file else { + return Ok(()); + }; + let mut kept: BTreeMap = self + .entries + .into_iter() + .filter(|(_, entry)| { + entry + .path + .as_ref() + .is_some_and(|path| !read_paths.contains(path)) + }) + .collect(); + kept.extend(self.fresh); + let bytes: Vec = serde_json::to_vec(&kept).map_err(|error| error.to_string())?; + let directory: &Path = file.parent().unwrap_or(&self.root); + let failed = |error: std::io::Error| format!("{}: {error}", directory.display()); + fs::create_dir_all(directory).map_err(failed)?; + fs::write( + self.root.join(CACHE_PARENT_IGNORE), + CACHE_PARENT_IGNORE_TEXT, + ) + .map_err(failed)?; + let staging: PathBuf = file.with_extension("tmp"); + fs::write(&staging, bytes).map_err(failed)?; + fs::rename(&staging, &file).map_err(failed) + } +} diff --git a/crates/sqlclean-cli/src/caching/tests.rs b/crates/sqlclean-cli/src/caching/tests.rs new file mode 100644 index 0000000..95523ad --- /dev/null +++ b/crates/sqlclean-cli/src/caching/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_result_cache.rs"] +mod result_cache; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean-cli/src/caching/tests/test_result_cache.rs b/crates/sqlclean-cli/src/caching/tests/test_result_cache.rs new file mode 100644 index 0000000..1041313 --- /dev/null +++ b/crates/sqlclean-cli/src/caching/tests/test_result_cache.rs @@ -0,0 +1,128 @@ +use std::collections::HashSet; +use std::fs; + +use crate::caching::main::cache_key::cache_key; +use crate::caching::models::ResultCache; +use crate::caching::tests::test_types::{CacheKeyTestCase, CacheRunTestCase}; + +const SEEDED: &str = r#"{"a":{"path":"a.sql","value":1},"b":{"path":"b.sql","value":2},"repo":{"path":null,"value":3}}"#; + +#[test] +fn given_a_cache_run_when_reopening_then_only_entries_for_unread_or_rewritten_files_remain() +-> Result<(), String> { + let test_cases = [ + CacheRunTestCase { + description: "a fresh cache keeps what it stored", + existing_cache: "", + enabled: true, + puts: &[("a", "a.sql", 1), ("b", "b.sql", 2)], + read_paths: &["a.sql", "b.sql"], + gets: &[], + expected_file_rewritten: true, + expected_reopened: &[("a", Some(1)), ("b", Some(2))], + }, + CacheRunTestCase { + description: "an entry read again survives and an unread file keeps its entry", + existing_cache: SEEDED, + enabled: true, + puts: &[], + read_paths: &["a.sql"], + gets: &["a"], + expected_file_rewritten: true, + expected_reopened: &[("a", Some(1)), ("b", Some(2)), ("repo", None)], + }, + CacheRunTestCase { + description: "a read file whose entry was not used is dropped", + existing_cache: SEEDED, + enabled: true, + puts: &[], + read_paths: &["a.sql", "b.sql"], + gets: &["b"], + expected_file_rewritten: true, + expected_reopened: &[("a", None), ("b", Some(2))], + }, + CacheRunTestCase { + description: "a damaged cache file reads as empty", + existing_cache: "{not json", + enabled: true, + puts: &[], + read_paths: &[], + gets: &["a"], + expected_file_rewritten: true, + expected_reopened: &[("a", None)], + }, + CacheRunTestCase { + description: "a disabled cache neither stores nor writes", + existing_cache: "", + enabled: false, + puts: &[("a", "a.sql", 1)], + read_paths: &["a.sql"], + gets: &["a"], + expected_file_rewritten: false, + expected_reopened: &[("a", None)], + }, + ]; + for test_case in &test_cases { + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + let cache_file = directory.path().join(".sqlclean/cache/results-v1.json"); + fs::create_dir_all(directory.path().join(".sqlclean/cache")) + .map_err(|error| error.to_string())?; + fs::write(&cache_file, test_case.existing_cache).map_err(|error| error.to_string())?; + let mut cache = ResultCache::open(directory.path(), test_case.enabled); + for key in test_case.gets { + let _value: Option = cache.get(key); + } + for (key, path, value) in test_case.puts { + cache.put(key, Some(path), value); + } + let read: HashSet = test_case + .read_paths + .iter() + .map(|path| (*path).to_owned()) + .collect(); + cache.save(&read)?; + let stored: String = fs::read_to_string(&cache_file).map_err(|error| error.to_string())?; + assert_eq!( + stored != test_case.existing_cache, + test_case.expected_file_rewritten, + "{}", + test_case.description + ); + let mut reopened = ResultCache::open(directory.path(), true); + for (key, expected) in test_case.expected_reopened { + assert_eq!( + reopened.get::(key), + *expected, + "{}: {key}", + test_case.description + ); + } + } + Ok(()) +} + +#[test] +fn given_key_parts_when_hashing_then_part_boundaries_change_the_key() { + let test_cases = [ + CacheKeyTestCase { + description: "moving a character between parts changes the key", + left: &["a", "bc"], + right: &["ab", "c"], + expected_equal: false, + }, + CacheKeyTestCase { + description: "the same parts give the same key", + left: &["a", "bc"], + right: &["a", "bc"], + expected_equal: true, + }, + ]; + for test_case in &test_cases { + assert_eq!( + cache_key(test_case.left) == cache_key(test_case.right), + test_case.expected_equal, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlclean-cli/src/caching/tests/test_types.rs b/crates/sqlclean-cli/src/caching/tests/test_types.rs new file mode 100644 index 0000000..25e6725 --- /dev/null +++ b/crates/sqlclean-cli/src/caching/tests/test_types.rs @@ -0,0 +1,17 @@ +pub(crate) struct CacheRunTestCase { + pub(crate) description: &'static str, + pub(crate) existing_cache: &'static str, + pub(crate) enabled: bool, + pub(crate) puts: &'static [(&'static str, &'static str, i32)], + pub(crate) read_paths: &'static [&'static str], + pub(crate) gets: &'static [&'static str], + pub(crate) expected_file_rewritten: bool, + pub(crate) expected_reopened: &'static [(&'static str, Option)], +} + +pub(crate) struct CacheKeyTestCase { + pub(crate) description: &'static str, + pub(crate) left: &'static [&'static str], + pub(crate) right: &'static [&'static str], + pub(crate) expected_equal: bool, +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/builtin.rs b/crates/sqlclean-cli/src/checking/_helpers/builtin.rs new file mode 100644 index 0000000..fa51aae --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/builtin.rs @@ -0,0 +1,88 @@ +//! The built-in Rules on one file, through the engine. + +use std::collections::BTreeSet; + +use sqlclean::sql_lint::constants::LINT_API_VERSION; +use sqlclean::sql_lint::main::fill_finding_path::fill_finding_path; +use sqlclean::sql_lint::main::lint::lint; +use sqlclean::sql_lint::models::{LintDiagnostic, LintRequest}; + +use crate::checking::constants::{ + PARSE_ERROR_CODE, PARSE_ERROR_MESSAGE, PARSE_ERROR_REMEDIATION, UNSAFE_FIX_BUILTIN_RULES, +}; +use crate::checking::models::{Finding, FixSafety}; +use crate::config::constants::CONFIG_FILE_NAME; +use crate::config::models::Thresholds; +use crate::discovery::models::SourceFile; +use crate::positions::models::Edit; + +/// The built-in findings of `file`, or one parse-error finding when the engine rejects it. +pub(crate) fn builtin_findings( + file: &SourceFile, + codes: &BTreeSet, + thresholds: Thresholds, +) -> Vec { + let request = LintRequest { + version: LINT_API_VERSION, + sql: file.text.clone(), + dialect: file.dialect.clone(), + enabled_rules: Some(codes.iter().cloned().collect()), + max_literal_length: Some(thresholds.max_literal_length), + max_ranking_order_by: Some(thresholds.max_ranking_order_by), + config_file: Some(CONFIG_FILE_NAME.to_owned()), + ..LintRequest::default() + }; + match lint(request) { + Ok(response) => response + .diagnostics + .into_iter() + .map(|diagnostic| finding(file, diagnostic)) + .collect(), + Err(error) => vec![parse_error(file, &error)], + } +} + +/// The finding for a file the parser rejects. +pub(crate) fn parse_error(file: &SourceFile, error: &str) -> Finding { + Finding { + code: PARSE_ERROR_CODE.to_owned(), + path: file.path.clone(), + start: 0, + end: 0, + message: format!("{PARSE_ERROR_MESSAGE} as {}: {error}", file.dialect), + remediation: PARSE_ERROR_REMEDIATION.to_owned(), + edits: Vec::new(), + fix: None, + } +} + +fn finding(file: &SourceFile, diagnostic: LintDiagnostic) -> Finding { + let edits: Vec = diagnostic + .fix + .into_iter() + .map(|edit| Edit { + start: edit.start, + end: edit.end, + text: edit.replacement, + }) + .collect(); + Finding { + code: diagnostic.code.to_owned(), + path: file.path.clone(), + start: diagnostic.start, + end: diagnostic.end, + message: diagnostic.message.into_owned(), + remediation: fill_finding_path(&diagnostic.remediation, &file.path), + fix: (!edits.is_empty()).then(|| builtin_fix_safety(diagnostic.code)), + edits, + } +} + +/// How safe the fixes of the built-in Rule `code` are. +pub(crate) fn builtin_fix_safety(code: &str) -> FixSafety { + if UNSAFE_FIX_BUILTIN_RULES.contains(&code) { + FixSafety::Unsafe + } else { + FixSafety::Safe + } +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/catalogue.rs b/crates/sqlclean-cli/src/checking/_helpers/catalogue.rs new file mode 100644 index 0000000..a2b60ee --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/catalogue.rs @@ -0,0 +1,109 @@ +//! The Rules a configuration can select: the built-ins and its custom Rules. + +use std::fs; +use std::path::{Path, PathBuf}; + +use sha2::{Digest, Sha256}; +use sqlclean::sql_lint::main::catalogue::catalogue; +use sqlclean::sql_lint::main::default_rules::default_rules; + +use crate::caching::main::cache_key::cache_key; +use crate::caching::models::ResultCache; +use crate::checking::_helpers::builtin::builtin_fix_safety; +use crate::checking::constants::FIXABLE_BUILTIN_RULES; +use crate::checking::models::{RuleInfo, RuleSubject}; +use crate::config::models::Config; +use crate::hosting::constants::CATALOGUE_MODE; +use crate::hosting::main::run_host::run_host; +use crate::hosting::models::HostRequest; + +const CATALOGUE_KEY: &str = "custom-catalogue"; +const PYTHON_SUFFIX: &str = "py"; +const PYTHON_CACHE_DIRECTORY: &str = "__pycache__"; + +pub(crate) fn builtin_rules() -> Vec { + let defaults: Vec<&str> = default_rules(); + catalogue() + .into_iter() + .map(|rule| RuleInfo { + code: rule.code.to_owned(), + message: rule.message.to_owned(), + remediation: rule.remediation.to_owned(), + enabled_by_default: defaults.contains(&rule.code), + custom: false, + subject: RuleSubject::Statement, + fixable: FIXABLE_BUILTIN_RULES + .contains(&rule.code) + .then(|| builtin_fix_safety(rule.code)), + module: None, + }) + .collect() +} + +/// A digest of every Python file under the configured custom Rule paths. +pub(crate) fn custom_sources_digest(config: &Config) -> Result { + let mut files: Vec = Vec::new(); + for entry in &config.rules.custom { + let path: PathBuf = config.root.join(entry); + if !path.exists() { + return Err(format!("[rules] custom path does not exist: {entry}")); + } + files.extend(python_files(&path)?); + } + files.sort(); + let mut digest = Sha256::new(); + for file in files { + let text: Vec = + fs::read(&file).map_err(|error| format!("{}: {error}", file.display()))?; + digest.update(file.to_string_lossy().as_bytes()); + digest.update(text.len().to_string().as_bytes()); + digest.update(&text); + } + Ok(format!("{:x}", digest.finalize())) +} + +/// The custom Rules, from the cache while their sources are unchanged, else from the host. +pub(crate) fn custom_rules( + config: &Config, + digest: &str, + cache: &mut ResultCache, +) -> Result, String> { + let key: String = cache_key(&[CATALOGUE_KEY, digest, &config.rules.custom.join("\n")]); + if let Some(rules) = cache.get::>(&key) { + return Ok(rules); + } + let response = run_host(&HostRequest { + mode: CATALOGUE_MODE.to_owned(), + root: config.root.to_string_lossy().into_owned(), + custom: config.rules.custom.clone(), + options: Default::default(), + files: Vec::new(), + repository_rules: Vec::new(), + })?; + cache.put(&key, None, &response.rules); + Ok(response.rules) +} + +/// The Python files at `path`, or under it when it is a directory. +fn python_files(path: &Path) -> Result, String> { + if path.is_file() { + let is_python: bool = path + .extension() + .is_some_and(|suffix| suffix == PYTHON_SUFFIX); + return Ok(if is_python { + vec![path.to_path_buf()] + } else { + Vec::new() + }); + } + let mut files: Vec = Vec::new(); + let entries = fs::read_dir(path).map_err(|error| format!("{}: {error}", path.display()))?; + for entry in entries { + let entry = entry.map_err(|error| error.to_string())?; + let name: String = entry.file_name().to_string_lossy().into_owned(); + if !name.starts_with('.') && name != PYTHON_CACHE_DIRECTORY { + files.extend(python_files(&entry.path())?); + } + } + Ok(files) +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/custom.rs b/crates/sqlclean-cli/src/checking/_helpers/custom.rs new file mode 100644 index 0000000..22d6a12 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/custom.rs @@ -0,0 +1,198 @@ +//! Custom Rules: cached per file, Rule and options, with the host started only for misses. + +use std::collections::{BTreeMap, BTreeSet}; + +use sha2::{Digest, Sha256}; +use sqlclean::sql_facts::main::file_facts::file_facts; + +use crate::caching::main::cache_key::cache_key; +use crate::caching::models::ResultCache; +use crate::checking::models::{Finding, FixSafety, RuleInfo, RuleSubject}; +use crate::config::models::Config; +use crate::discovery::models::SourceFile; +use crate::hosting::constants::EVALUATE_MODE; +use crate::hosting::main::run_host::run_host; +use crate::hosting::models::{HostFile, HostFinding, HostRequest}; + +const FILE_RESULT_KEY: &str = "custom-file"; +const REPOSITORY_RESULT_KEY: &str = "custom-repository"; + +/// What custom evaluation needs besides the files. +pub(crate) struct CustomEvaluation<'a> { + pub(crate) config: &'a Config, + pub(crate) rules: Vec<&'a RuleInfo>, + pub(crate) sources_digest: &'a str, +} + +/// The findings of the selected custom Rules on `files`, which must all parse. +pub(crate) fn custom_findings( + evaluation: &CustomEvaluation<'_>, + files: &[&SourceFile], + cache: &mut ResultCache, +) -> Result, String> { + let options: BTreeMap = rule_options(evaluation)?; + let (file_rules, repository_rules): (Vec<&RuleInfo>, Vec<&RuleInfo>) = evaluation + .rules + .iter() + .partition(|rule| rule.subject != RuleSubject::Repository); + let mut findings: Vec = Vec::new(); + let mut misses: Vec<(String, Option, String)> = Vec::new(); + let mut host_files: Vec = Vec::new(); + for file in files { + let mut missing: Vec = Vec::new(); + for rule in &file_rules { + let key: String = file_key(evaluation, rule, &options, file); + match cache.get::>(&key) { + Some(cached) => { + findings.extend(cached.into_iter().map(|found| finding(rule, found))) + } + None => { + missing.push(rule.code.clone()); + misses.push((key, Some(file.path.clone()), rule.code.clone())); + } + } + } + if !missing.is_empty() { + host_files.push(host_file(file, missing)?); + } + } + let repository_digest: String = repository_digest(files); + let mut missing_repository: Vec = Vec::new(); + for rule in &repository_rules { + let key: String = cache_key(&[ + REPOSITORY_RESULT_KEY, + evaluation.sources_digest, + &rule.code, + &options_text(&options, &rule.code), + &repository_digest, + ]); + match cache.get::>(&key) { + Some(cached) => findings.extend(cached.into_iter().map(|found| finding(rule, found))), + None => { + missing_repository.push(rule.code.clone()); + misses.push((key, None, rule.code.clone())); + } + } + } + if misses.is_empty() { + return Ok(findings); + } + if !missing_repository.is_empty() { + host_files = with_every_file(files, host_files)?; + } + let response = run_host(&HostRequest { + mode: EVALUATE_MODE.to_owned(), + root: evaluation.config.root.to_string_lossy().into_owned(), + custom: evaluation.config.rules.custom.clone(), + options, + files: host_files, + repository_rules: missing_repository, + })?; + let mut by_subject: BTreeMap<(Option, String), Vec> = BTreeMap::new(); + for result in response.results { + by_subject.insert((result.path, result.code), result.findings); + } + for (key, path, code) in misses { + let found: Vec = by_subject + .remove(&(path.clone(), code.clone())) + .unwrap_or_default(); + cache.put(&key, path.as_deref(), &found); + if let Some(rule) = evaluation.rules.iter().find(|rule| rule.code == code) { + findings.extend(found.into_iter().map(|item| finding(rule, item))); + } + } + Ok(findings) +} + +fn rule_options( + evaluation: &CustomEvaluation<'_>, +) -> Result, String> { + let codes: BTreeSet<&str> = evaluation + .rules + .iter() + .map(|rule| rule.code.as_str()) + .collect(); + let mut options: BTreeMap = BTreeMap::new(); + for (code, table) in &evaluation.config.rules.rule_options { + if codes.contains(code.as_str()) { + let value = serde_json::to_value(table) + .map_err(|error| format!("[rules.rule_options.{code}]: {error}"))?; + options.insert(code.clone(), value); + } + } + Ok(options) +} + +fn options_text(options: &BTreeMap, code: &str) -> String { + options + .get(code) + .map(ToString::to_string) + .unwrap_or_default() +} + +fn file_key( + evaluation: &CustomEvaluation<'_>, + rule: &RuleInfo, + options: &BTreeMap, + file: &SourceFile, +) -> String { + cache_key(&[ + FILE_RESULT_KEY, + evaluation.sources_digest, + &rule.code, + &options_text(options, &rule.code), + &file.path, + &file.dialect, + &file.text, + ]) +} + +fn host_file(file: &SourceFile, rules: Vec) -> Result { + Ok(HostFile { + path: file.path.clone(), + source: file.text.clone(), + dialect: file.dialect.clone(), + facts: file_facts(&file.text, &file.dialect)?, + rules, + }) +} + +/// A repository Rule reads every file, including those whose own Rules were all cached. +fn with_every_file( + files: &[&SourceFile], + mut host_files: Vec, +) -> Result, String> { + for file in files { + if !host_files.iter().any(|host| host.path == file.path) { + host_files.push(host_file(file, Vec::new())?); + } + } + host_files.sort_by(|left, right| left.path.cmp(&right.path)); + Ok(host_files) +} + +fn repository_digest(files: &[&SourceFile]) -> String { + let mut digest = Sha256::new(); + for file in files { + for part in [&file.path, &file.dialect, &file.text] { + digest.update(part.len().to_string().as_bytes()); + digest.update(part.as_bytes()); + } + } + format!("{:x}", digest.finalize()) +} + +fn finding(rule: &RuleInfo, found: HostFinding) -> Finding { + let fix: Option = + (!found.edits.is_empty()).then(|| rule.fixable.unwrap_or(FixSafety::Safe)); + Finding { + code: rule.code.clone(), + path: found.path, + start: found.start, + end: found.end, + message: found.message, + remediation: found.remediation, + edits: found.edits, + fix, + } +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/exemptions.rs b/crates/sqlclean-cli/src/checking/_helpers/exemptions.rs new file mode 100644 index 0000000..218b1ac --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/exemptions.rs @@ -0,0 +1,97 @@ +//! Configured path ignores and reviewed exceptions, and exceptions that no longer match. + +use std::collections::{BTreeSet, HashSet}; + +use globset::GlobSet; +use sqlclean::sql_lint::main::cross_join_subject::cross_join_subject; + +use crate::checking::constants::{CROSS_JOIN_CODE, PARSE_ERROR_CODE}; +use crate::checking::models::{Finding, StaleException}; +use crate::config::models::{RuleException, RulesConfig}; +use crate::discovery::main::path_patterns::path_patterns; + +/// Findings left after ignores and exceptions, and the exceptions this check proves stale. +pub(crate) fn apply_exemptions( + findings: Vec, + rules: &RulesConfig, + checked_paths: &HashSet, + selected: &BTreeSet, +) -> Result<(Vec, Vec), String> { + let mut ignores: Vec<(GlobSet, &[String])> = Vec::new(); + for ignore in &rules.rule_ignores { + ignores.push((path_patterns(&ignore.paths)?, ignore.rules.as_slice())); + } + let mut used: HashSet = HashSet::new(); + let kept: Vec = findings + .into_iter() + .filter(|finding| finding.code == PARSE_ERROR_CODE || !is_ignored(finding, &ignores)) + .filter( + |finding| match exception_for(finding, &rules.rule_exceptions) { + Some(index) => { + used.insert(index); + false + } + None => true, + }, + ) + .collect(); + let stale: Vec = rules + .rule_exceptions + .iter() + .enumerate() + .filter(|(index, exception)| { + !used.contains(index) + && checked_paths.contains(&exception.path) + && selected.contains(&exception.rule) + }) + .map(|(_, exception)| StaleException { + rule: exception.rule.clone(), + path: exception.path.clone(), + relation: exception.relation.clone(), + }) + .collect(); + Ok((kept, stale)) +} + +fn is_ignored(finding: &Finding, ignores: &[(GlobSet, &[String])]) -> bool { + ignores + .iter() + .any(|(paths, codes)| paths.is_match(&finding.path) && selects(codes, &finding.code)) +} + +fn selects(selectors: &[String], code: &str) -> bool { + selectors + .iter() + .any(|selector| code.starts_with(selector.as_str())) +} + +/// The index of the exception that covers `finding`; parse errors are never excepted. +fn exception_for(finding: &Finding, exceptions: &[RuleException]) -> Option { + if finding.code == PARSE_ERROR_CODE { + return None; + } + exceptions + .iter() + .position(|exception| covers(exception, finding)) +} + +fn covers(exception: &RuleException, finding: &Finding) -> bool { + if exception.rule != finding.code || exception.path != finding.path { + return false; + } + let Some(relation) = &exception.relation else { + return true; + }; + if finding.code != CROSS_JOIN_CODE { + return false; + } + let Some((joined, cte)) = cross_join_subject(&finding.message) else { + return false; + }; + let cte_matches: bool = match (&exception.cte, cte) { + (None, _) => true, + (Some(expected), Some(cte)) => cte.eq_ignore_ascii_case(expected), + (Some(_), None) => false, + }; + joined.eq_ignore_ascii_case(relation) && cte_matches +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/mod.rs b/crates/sqlclean-cli/src/checking/_helpers/mod.rs new file mode 100644 index 0000000..d9cb031 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/mod.rs @@ -0,0 +1,6 @@ +pub mod builtin; +pub mod catalogue; +pub mod custom; +pub mod exemptions; +pub mod orchestration; +pub mod selection; diff --git a/crates/sqlclean-cli/src/checking/_helpers/orchestration.rs b/crates/sqlclean-cli/src/checking/_helpers/orchestration.rs new file mode 100644 index 0000000..6fc3a19 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/orchestration.rs @@ -0,0 +1,179 @@ +//! One check: select Rules, run the built-ins, then custom Rules on files that parse. + +use std::collections::{BTreeSet, HashSet}; + +use rayon::iter::{IndexedParallelIterator, IntoParallelRefIterator, ParallelIterator}; + +use crate::caching::main::cache_key::cache_key; +use crate::caching::models::ResultCache; +use crate::checking::_helpers::builtin::builtin_findings; +use crate::checking::_helpers::catalogue::{builtin_rules, custom_rules, custom_sources_digest}; +use crate::checking::_helpers::custom::{CustomEvaluation, custom_findings}; +use crate::checking::_helpers::exemptions::apply_exemptions; +use crate::checking::_helpers::selection::selected_codes; +use crate::checking::constants::{CUSTOM_RULE_PREFIX, PARSE_ERROR_CODE}; +use crate::checking::models::{CheckOptions, CheckOutcome, Finding, RuleInfo}; +use crate::config::models::Config; +use crate::discovery::models::SourceFile; + +const BUILTIN_RESULT_KEY: &str = "builtin"; + +pub(crate) fn check_sources( + config: &Config, + files: &[SourceFile], + options: &CheckOptions, +) -> Result { + let mut cache = ResultCache::open(&config.root, options.use_cache); + let select: Option<&[String]> = options.select.as_deref().or(config.rules.select.as_deref()); + let builtins: Vec = builtin_rules(); + let needs_custom: bool = + !config.rules.custom.is_empty() && select.is_none_or(selects_custom_rules); + let digest: String = if needs_custom { + custom_sources_digest(config)? + } else { + String::new() + }; + let custom: Vec = if needs_custom { + custom_rules(config, &digest, &mut cache)? + } else { + Vec::new() + }; + validate_rule_options(config, &builtins, needs_custom.then_some(custom.as_slice()))?; + let all: Vec = builtins.iter().chain(&custom).cloned().collect(); + let ignore: Vec = config + .rules + .ignore + .iter() + .chain(&options.ignore) + .filter(|selector| needs_custom || !selector.starts_with('X')) + .cloned() + .collect(); + let selected: BTreeSet = selected_codes(&all, select, &ignore)?; + let (mut findings, updated) = run_builtins( + &BuiltinRun { + config, + files, + builtins: &builtins, + selected: &selected, + }, + cache, + ); + let mut cache: ResultCache = updated; + let unparsed: HashSet<&str> = findings + .iter() + .filter(|finding| finding.code == PARSE_ERROR_CODE) + .map(|finding| finding.path.as_str()) + .collect(); + let parsed: Vec<&SourceFile> = files + .iter() + .filter(|file| !unparsed.contains(file.path.as_str())) + .collect(); + let custom_selected: Vec<&RuleInfo> = custom + .iter() + .filter(|rule| selected.contains(&rule.code)) + .collect(); + if !custom_selected.is_empty() { + let evaluation = CustomEvaluation { + config, + rules: custom_selected, + sources_digest: &digest, + }; + findings.extend(custom_findings(&evaluation, &parsed, &mut cache)?); + } + let checked: HashSet = files.iter().map(|file| file.path.clone()).collect(); + let (mut kept, stale_exceptions) = + apply_exemptions(findings, &config.rules, &checked, &selected)?; + kept.sort_by(|left, right| { + (&left.path, left.start, left.end, &left.code).cmp(&( + &right.path, + right.start, + right.end, + &right.code, + )) + }); + cache + .save(&checked) + .map_err(|error| format!("could not write the result cache (use --no-cache): {error}"))?; + Ok(CheckOutcome { + findings: kept, + stale_exceptions, + }) +} + +/// Whether a selection can match custom Rules: an `XSQC` code or a prefix of `XSQC`. +fn selects_custom_rules(selectors: &[String]) -> bool { + selectors.iter().any(|selector| { + selector.starts_with(CUSTOM_RULE_PREFIX) + || CUSTOM_RULE_PREFIX.starts_with(selector.as_str()) + }) +} + +/// The inputs of one built-in Rule run. +struct BuiltinRun<'a> { + config: &'a Config, + files: &'a [SourceFile], + builtins: &'a [RuleInfo], + selected: &'a BTreeSet, +} + +fn run_builtins(run: &BuiltinRun<'_>, mut cache: ResultCache) -> (Vec, ResultCache) { + let codes: BTreeSet = run + .builtins + .iter() + .filter(|rule| run.selected.contains(&rule.code)) + .map(|rule| rule.code.clone()) + .collect(); + let selection: String = codes.iter().cloned().collect::>().join(","); + let thresholds = run.config.rules.thresholds; + let limits: String = format!( + "{}:{}", + thresholds.max_literal_length, thresholds.max_ranking_order_by + ); + let keys: Vec = run + .files + .iter() + .map(|file| { + cache_key(&[ + BUILTIN_RESULT_KEY, + &selection, + &limits, + &file.path, + &file.dialect, + &file.text, + ]) + }) + .collect(); + let cached: Vec>> = keys.iter().map(|key| cache.get(key)).collect(); + let computed: Vec> = run + .files + .par_iter() + .zip(cached) + .map(|(file, cached)| cached.unwrap_or_else(|| builtin_findings(file, &codes, thresholds))) + .collect(); + for ((file, key), found) in run.files.iter().zip(&keys).zip(&computed) { + cache.put(key, Some(&file.path), found); + } + (computed.into_iter().flatten().collect(), cache) +} + +fn validate_rule_options( + config: &Config, + builtins: &[RuleInfo], + custom: Option<&[RuleInfo]>, +) -> Result<(), String> { + for code in config.rules.rule_options.keys() { + if builtins.iter().any(|rule| &rule.code == code) { + return Err(format!( + "[rules.rule_options.{code}]: built-in Rules take no options; set limits in [rules.thresholds]" + )); + } + if let Some(custom) = custom + && !custom.iter().any(|rule| &rule.code == code) + { + return Err(format!( + "[rules.rule_options.{code}]: no custom Rule has this code" + )); + } + } + Ok(()) +} diff --git a/crates/sqlclean-cli/src/checking/_helpers/selection.rs b/crates/sqlclean-cli/src/checking/_helpers/selection.rs new file mode 100644 index 0000000..04d4523 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/_helpers/selection.rs @@ -0,0 +1,39 @@ +//! Resolve `select` and `ignore` selectors, exact codes or family prefixes, to Rule codes. + +use std::collections::BTreeSet; + +use crate::checking::models::RuleInfo; + +pub(crate) fn selected_codes( + rules: &[RuleInfo], + select: Option<&[String]>, + ignore: &[String], +) -> Result, String> { + let mut selected: BTreeSet = match select { + None => rules + .iter() + .filter(|rule| rule.enabled_by_default) + .map(|rule| rule.code.clone()) + .collect(), + Some(selectors) => expand(rules, selectors)?, + }; + for code in expand(rules, ignore)? { + selected.remove(&code); + } + Ok(selected) +} + +fn expand(rules: &[RuleInfo], selectors: &[String]) -> Result, String> { + let mut codes: BTreeSet = BTreeSet::new(); + for selector in selectors { + let matches: Vec<&RuleInfo> = rules + .iter() + .filter(|rule| rule.code.starts_with(selector.as_str())) + .collect(); + if matches.is_empty() { + return Err(format!("Rule selector '{selector}' matches no Rule")); + } + codes.extend(matches.into_iter().map(|rule| rule.code.clone())); + } + Ok(codes) +} diff --git a/crates/sqlclean-cli/src/checking/constants.rs b/crates/sqlclean-cli/src/checking/constants.rs new file mode 100644 index 0000000..d0cb12f --- /dev/null +++ b/crates/sqlclean-cli/src/checking/constants.rs @@ -0,0 +1,15 @@ +/// The finding for a file the parser rejects; it cannot be deselected or excepted. +pub const PARSE_ERROR_CODE: &str = "SQC999"; +pub(crate) const PARSE_ERROR_MESSAGE: &str = "SQL could not be parsed"; +pub(crate) const PARSE_ERROR_REMEDIATION: &str = + "Fix the syntax error, or set this file's dialect in sqlclean.toml."; +pub(crate) const CROSS_JOIN_CODE: &str = "SQC038"; +/// Custom Rule codes start with this prefix. +pub const CUSTOM_RULE_PREFIX: &str = "XSQC"; +/// Built-in Rules whose findings can carry a fix. +pub(crate) const FIXABLE_BUILTIN_RULES: [&str; 10] = [ + "SQC001", "SQC002", "SQC003", "SQC005", "SQC006", "SQC008", "SQC013", "SQC017", "SQC025", + "SQC030", +]; +/// Built-in Rules whose fix can change query results, so `--unsafe-fixes` must opt in. +pub(crate) const UNSAFE_FIX_BUILTIN_RULES: [&str; 1] = ["SQC001"]; diff --git a/crates/sqlclean-cli/src/checking/main/check_sources.rs b/crates/sqlclean-cli/src/checking/main/check_sources.rs new file mode 100644 index 0000000..a02c603 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/main/check_sources.rs @@ -0,0 +1,14 @@ +//! Check SQL files against the configured Rules. + +use crate::checking::models::{CheckOptions, CheckOutcome}; +use crate::config::models::Config; +use crate::discovery::models::SourceFile; + +/// The findings of the selected Rules on `files`, after ignores and exceptions. +pub fn check_sources( + config: &Config, + files: &[SourceFile], + options: &CheckOptions, +) -> Result { + crate::checking::_helpers::orchestration::check_sources(config, files, options) +} diff --git a/crates/sqlclean-cli/src/checking/main/mod.rs b/crates/sqlclean-cli/src/checking/main/mod.rs new file mode 100644 index 0000000..5c960b7 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/main/mod.rs @@ -0,0 +1,2 @@ +pub mod check_sources; +pub mod rule_catalogue; diff --git a/crates/sqlclean-cli/src/checking/main/rule_catalogue.rs b/crates/sqlclean-cli/src/checking/main/rule_catalogue.rs new file mode 100644 index 0000000..29fcb6b --- /dev/null +++ b/crates/sqlclean-cli/src/checking/main/rule_catalogue.rs @@ -0,0 +1,18 @@ +//! Every Rule a configuration can select, for listing and inspection. + +use crate::caching::models::ResultCache; +use crate::checking::_helpers::catalogue::{builtin_rules, custom_rules, custom_sources_digest}; +use crate::checking::models::RuleInfo; +use crate::config::models::Config; + +/// The built-in Rules followed by the configuration's custom Rules. +pub fn rule_catalogue(config: &Config, use_cache: bool) -> Result, String> { + let mut rules: Vec = builtin_rules(); + if !config.rules.custom.is_empty() { + let mut cache = ResultCache::open(&config.root, use_cache); + let digest: String = custom_sources_digest(config)?; + rules.extend(custom_rules(config, &digest, &mut cache)?); + cache.save(&Default::default())?; + } + Ok(rules) +} diff --git a/crates/sqlclean-cli/src/checking/mod.rs b/crates/sqlclean-cli/src/checking/mod.rs new file mode 100644 index 0000000..2f922c4 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/mod.rs @@ -0,0 +1,6 @@ +//! Evaluate the selected built-in and custom Rules over SQL files. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; diff --git a/crates/sqlclean-cli/src/checking/models.rs b/crates/sqlclean-cli/src/checking/models.rs new file mode 100644 index 0000000..85b95b5 --- /dev/null +++ b/crates/sqlclean-cli/src/checking/models.rs @@ -0,0 +1,77 @@ +//! Findings, Rule metadata and the result of one check. + +use serde::{Deserialize, Serialize}; + +use crate::positions::models::Edit; + +/// Whether a fix keeps the query's meaning (`safe`) or may change it (`unsafe`). +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FixSafety { + Safe, + Unsafe, +} + +/// One Rule violation, located by character offsets in its file. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct Finding { + pub code: String, + pub path: String, + pub start: usize, + pub end: usize, + pub message: String, + pub remediation: String, + #[serde(default, skip_serializing_if = "Vec::is_empty")] + pub edits: Vec, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fix: Option, +} + +/// The subject a Rule evaluates. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RuleSubject { + Statement, + File, + Repository, +} + +/// One built-in or custom Rule. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct RuleInfo { + pub code: String, + pub message: String, + pub remediation: String, + pub enabled_by_default: bool, + pub custom: bool, + pub subject: RuleSubject, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub fixable: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub module: Option, +} + +/// A configured exception that matched no finding. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct StaleException { + pub rule: String, + pub path: String, + pub relation: Option, +} + +/// Every finding of one check, sorted by path, position and code. +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)] +pub struct CheckOutcome { + pub findings: Vec, + pub stale_exceptions: Vec, +} + +/// What a check evaluates beyond the configuration. +#[derive(Debug, Clone, Default)] +pub struct CheckOptions { + /// Replaces the configured `[rules] select` when given. + pub select: Option>, + /// Added to the configured `[rules] ignore`. + pub ignore: Vec, + pub use_cache: bool, +} diff --git a/crates/sqlclean-cli/src/command/_helpers/check_command.rs b/crates/sqlclean-cli/src/command/_helpers/check_command.rs new file mode 100644 index 0000000..25b6b36 --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/check_command.rs @@ -0,0 +1,33 @@ +//! `sqlclean check`. + +use std::collections::HashMap; +use std::io::Write; + +use crate::checking::main::check_sources::check_sources; +use crate::checking::models::CheckOptions; +use crate::command::constants::{EXIT_FINDINGS, EXIT_SUCCESS}; +use crate::command::models::{CheckArgs, SelectionArgs}; +use crate::config::models::Config; +use crate::discovery::main::discover_files::discover_files; +use crate::reporting::main::render_findings::render_findings; + +pub(crate) fn run(config: &Config, arguments: &CheckArgs) -> Result { + let files = discover_files(config, &arguments.selection.paths)?; + let outcome = check_sources(config, &files, &check_options(&arguments.selection))?; + let texts: HashMap = files + .into_iter() + .map(|file| (file.path, file.text)) + .collect(); + let report: String = render_findings(&outcome, &texts, arguments.output_format); + let _ = std::io::stdout().write_all(report.as_bytes()); + let clean: bool = outcome.findings.is_empty() && outcome.stale_exceptions.is_empty(); + Ok(if clean { EXIT_SUCCESS } else { EXIT_FINDINGS }) +} + +pub(crate) fn check_options(selection: &SelectionArgs) -> CheckOptions { + CheckOptions { + select: selection.select.clone(), + ignore: selection.ignore.clone(), + use_cache: !selection.no_cache, + } +} diff --git a/crates/sqlclean-cli/src/command/_helpers/diffs.rs b/crates/sqlclean-cli/src/command/_helpers/diffs.rs new file mode 100644 index 0000000..e4447fe --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/diffs.rs @@ -0,0 +1,25 @@ +//! Unified diffs and file writes for commands that change files. + +use std::fs; +use std::io::Write; + +use similar::TextDiff; + +use crate::discovery::models::SourceFile; + +const CONTEXT_LINES: usize = 3; + +pub(crate) fn print_diff(file: &SourceFile, changed: &str) { + let diff = TextDiff::from_lines(file.text.as_str(), changed); + let rendered: String = diff + .unified_diff() + .context_radius(CONTEXT_LINES) + .header(&format!("a/{}", file.path), &format!("b/{}", file.path)) + .to_string(); + let _ = std::io::stdout().write_all(rendered.as_bytes()); +} + +pub(crate) fn write_file(file: &SourceFile, changed: &str) -> Result<(), String> { + fs::write(&file.absolute, changed) + .map_err(|error| format!("could not write {}: {error}", file.path)) +} diff --git a/crates/sqlclean-cli/src/command/_helpers/dispatch.rs b/crates/sqlclean-cli/src/command/_helpers/dispatch.rs new file mode 100644 index 0000000..1a76862 --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/dispatch.rs @@ -0,0 +1,55 @@ +//! Argument parsing and the error boundary every subcommand shares. + +use std::env; +use std::ffi::OsString; +use std::io::Write; +use std::path::PathBuf; + +use clap::Parser; + +use crate::command::constants::{EXIT_ERROR, EXIT_SUCCESS}; +use crate::command::models::{Cli, Command}; +use crate::config::main::load_config::load_config; +use crate::config::models::Config; + +pub(crate) fn run(arguments: Vec) -> u8 { + let cli: Cli = match Cli::try_parse_from(arguments) { + Ok(cli) => cli, + Err(error) => { + let _ = error.print(); + return if error.use_stderr() { + EXIT_ERROR + } else { + EXIT_SUCCESS + }; + } + }; + match execute(cli) { + Ok(code) => code, + Err(error) => { + let _ = writeln!(std::io::stderr(), "sqlclean: error: {error}"); + EXIT_ERROR + } + } +} + +fn execute(cli: Cli) -> Result { + if let Command::Facts(arguments) = &cli.command { + return crate::command::_helpers::facts_command::run(arguments); + } + let start: PathBuf = env::current_dir().map_err(|error| error.to_string())?; + let config: Config = load_config(cli.config.as_deref(), &start)?; + match cli.command { + Command::Check(arguments) => { + crate::command::_helpers::check_command::run(&config, &arguments) + } + Command::Fix(arguments) => crate::command::_helpers::fix_command::run(&config, &arguments), + Command::Format(arguments) => { + crate::command::_helpers::format_command::run(&config, &arguments) + } + Command::Rules(arguments) => { + crate::command::_helpers::rules_command::run(&config, &arguments) + } + Command::Facts(_) => Ok(EXIT_SUCCESS), + } +} diff --git a/crates/sqlclean-cli/src/command/_helpers/facts_command.rs b/crates/sqlclean-cli/src/command/_helpers/facts_command.rs new file mode 100644 index 0000000..325eb80 --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/facts_command.rs @@ -0,0 +1,19 @@ +//! `sqlclean facts`: the statement facts of SQL on standard input, for Rule test harnesses. + +use std::io::{Read, Write}; + +use sqlclean::sql_facts::main::file_facts::file_facts; + +use crate::command::constants::EXIT_SUCCESS; +use crate::command::models::FactsArgs; + +pub(crate) fn run(arguments: &FactsArgs) -> Result { + let mut sql = String::new(); + std::io::stdin() + .read_to_string(&mut sql) + .map_err(|error| error.to_string())?; + let facts = file_facts(&sql, &arguments.dialect)?; + let json: String = serde_json::to_string(&facts).map_err(|error| error.to_string())?; + let _ = writeln!(std::io::stdout(), "{json}"); + Ok(EXIT_SUCCESS) +} diff --git a/crates/sqlclean-cli/src/command/_helpers/fix_command.rs b/crates/sqlclean-cli/src/command/_helpers/fix_command.rs new file mode 100644 index 0000000..53ed26c --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/fix_command.rs @@ -0,0 +1,70 @@ +//! `sqlclean fix`. + +use std::collections::HashMap; +use std::io::Write; + +use crate::command::_helpers::check_command::check_options; +use crate::command::_helpers::diffs::{print_diff, write_file}; +use crate::command::constants::{EXIT_FINDINGS, EXIT_SUCCESS}; +use crate::command::models::FixArgs; +use crate::config::models::Config; +use crate::discovery::main::discover_files::discover_files; +use crate::fixing::main::fix_sources::fix_sources; +use crate::reporting::main::render_findings::render_findings; +use crate::reporting::models::OutputFormat; + +pub(crate) fn run(config: &Config, arguments: &FixArgs) -> Result { + let originals = discover_files(config, &arguments.selection.paths)?; + let outcome = fix_sources( + config, + originals.clone(), + &check_options(&arguments.selection), + arguments.unsafe_fixes, + )?; + let mut changed_files: usize = 0; + for (original, fixed) in originals.iter().zip(&outcome.files) { + if original.text == fixed.text { + continue; + } + changed_files += 1; + if arguments.diff { + print_diff(original, &fixed.text); + } else { + write_file(original, &fixed.text)?; + } + } + let mut stderr = std::io::stderr(); + for (path, reason) in &outcome.format_refusals { + let _ = writeln!(stderr, "{path}: could not format: {reason}"); + } + let summary: &str = if arguments.diff { + "Would fix {fixes} problem(s) and change {files} file(s)." + } else { + "Fixed {fixes} problem(s) and changed {files} file(s)." + }; + let _ = writeln!( + stderr, + "{}", + summary + .replace("{fixes}", &outcome.applied_fixes.to_string()) + .replace("{files}", &changed_files.to_string()) + ); + if !arguments.diff { + let texts: HashMap = outcome + .files + .iter() + .map(|file| (file.path.clone(), file.text.clone())) + .collect(); + let report: String = render_findings(&outcome.remaining, &texts, OutputFormat::Text); + let _ = std::io::stdout().write_all(report.as_bytes()); + } + let remaining: bool = !outcome.remaining.findings.is_empty() + || !outcome.remaining.stale_exceptions.is_empty() + || !outcome.format_refusals.is_empty() + || (arguments.diff && changed_files > 0); + Ok(if remaining { + EXIT_FINDINGS + } else { + EXIT_SUCCESS + }) +} diff --git a/crates/sqlclean-cli/src/command/_helpers/format_command.rs b/crates/sqlclean-cli/src/command/_helpers/format_command.rs new file mode 100644 index 0000000..5ce2aca --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/format_command.rs @@ -0,0 +1,49 @@ +//! `sqlclean format`. + +use std::io::Write; + +use crate::command::_helpers::diffs::{print_diff, write_file}; +use crate::command::constants::{EXIT_FINDINGS, EXIT_SUCCESS}; +use crate::command::models::FormatArgs; +use crate::config::models::Config; +use crate::discovery::main::discover_files::discover_files; +use crate::formatting::main::format_source::format_source; + +pub(crate) fn run(config: &Config, arguments: &FormatArgs) -> Result { + let files = discover_files(config, &arguments.paths)?; + let mut stderr = std::io::stderr(); + let (mut changed, mut refused): (usize, usize) = (0, 0); + for file in &files { + let formatted: String = match format_source(&file.text, &file.dialect, config.line_width) { + Ok(formatted) => formatted, + Err(reason) => { + refused += 1; + let _ = writeln!(stderr, "{}: could not format: {reason}", file.path); + continue; + } + }; + if formatted == file.text { + continue; + } + changed += 1; + if arguments.diff { + print_diff(file, &formatted); + } else if arguments.check { + let _ = writeln!(std::io::stdout(), "Would reformat: {}", file.path); + } else { + write_file(file, &formatted)?; + } + } + let unchanged: usize = files.len() - changed - refused; + let verb: &str = if arguments.check || arguments.diff { + "would be reformatted" + } else { + "reformatted" + }; + let _ = writeln!( + stderr, + "{changed} file(s) {verb}, {unchanged} file(s) already formatted." + ); + let failed: bool = refused > 0 || ((arguments.check || arguments.diff) && changed > 0); + Ok(if failed { EXIT_FINDINGS } else { EXIT_SUCCESS }) +} diff --git a/crates/sqlclean-cli/src/command/_helpers/mod.rs b/crates/sqlclean-cli/src/command/_helpers/mod.rs new file mode 100644 index 0000000..a54f409 --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/mod.rs @@ -0,0 +1,7 @@ +pub mod check_command; +pub mod diffs; +pub mod dispatch; +pub mod facts_command; +pub mod fix_command; +pub mod format_command; +pub mod rules_command; diff --git a/crates/sqlclean-cli/src/command/_helpers/rules_command.rs b/crates/sqlclean-cli/src/command/_helpers/rules_command.rs new file mode 100644 index 0000000..b23799a --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/rules_command.rs @@ -0,0 +1,77 @@ +//! `sqlclean rules`. + +use std::fmt::Write as _; +use std::io::Write; + +use crate::checking::main::rule_catalogue::rule_catalogue; +use crate::checking::models::RuleInfo; +use crate::command::constants::{EXIT_ERROR, EXIT_SUCCESS}; +use crate::command::models::RulesArgs; +use crate::config::models::Config; + +pub(crate) fn run(config: &Config, arguments: &RulesArgs) -> Result { + let rules: Vec = rule_catalogue(config, true)?; + let shown: Vec<&RuleInfo> = match &arguments.code { + Some(code) => rules.iter().filter(|rule| &rule.code == code).collect(), + None => rules.iter().collect(), + }; + if shown.is_empty() { + let _ = writeln!(std::io::stderr(), "sqlclean: error: unknown Rule code"); + return Ok(EXIT_ERROR); + } + let output: String = if arguments.json { + serde_json::to_string_pretty(&shown).map_err(|error| error.to_string())? + "\n" + } else if arguments.code.is_some() { + detail(shown[0]) + } else { + table(&shown) + }; + let _ = std::io::stdout().write_all(output.as_bytes()); + Ok(EXIT_SUCCESS) +} + +fn table(rules: &[&RuleInfo]) -> String { + let mut output = String::new(); + for rule in rules { + let default: &str = if rule.enabled_by_default { + "default" + } else { + "" + }; + let _ = writeln!(output, "{:<12} {:<8} {}", rule.code, default, rule.message); + } + output +} + +fn detail(rule: &RuleInfo) -> String { + let mut output = String::new(); + let _ = writeln!(output, "{}: {}", rule.code, rule.message); + let _ = writeln!( + output, + "Subject: {}", + serde_json::to_value(rule.subject) + .unwrap_or_default() + .as_str() + .unwrap_or_default() + ); + let _ = writeln!( + output, + "Enabled by default: {}", + if rule.enabled_by_default { "yes" } else { "no" } + ); + if let Some(module) = &rule.module { + let _ = writeln!(output, "Defined in: {module}"); + } + if let Some(fixable) = rule.fixable { + let _ = writeln!( + output, + "Fixable: {}", + serde_json::to_value(fixable) + .unwrap_or_default() + .as_str() + .unwrap_or_default() + ); + } + let _ = writeln!(output, "Remediation: {}", rule.remediation); + output +} diff --git a/crates/sqlclean-cli/src/command/constants.rs b/crates/sqlclean-cli/src/command/constants.rs new file mode 100644 index 0000000..3c6397c --- /dev/null +++ b/crates/sqlclean-cli/src/command/constants.rs @@ -0,0 +1,5 @@ +pub const EXIT_SUCCESS: u8 = 0; +/// Findings remain, or files need changes that `--check` or `--diff` did not write. +pub const EXIT_FINDINGS: u8 = 1; +/// The command could not run: invalid arguments, configuration or input. +pub const EXIT_ERROR: u8 = 2; diff --git a/crates/sqlclean-cli/src/command/main/mod.rs b/crates/sqlclean-cli/src/command/main/mod.rs new file mode 100644 index 0000000..9137f27 --- /dev/null +++ b/crates/sqlclean-cli/src/command/main/mod.rs @@ -0,0 +1 @@ +pub mod run; diff --git a/crates/sqlclean-cli/src/command/main/run.rs b/crates/sqlclean-cli/src/command/main/run.rs new file mode 100644 index 0000000..117bbb6 --- /dev/null +++ b/crates/sqlclean-cli/src/command/main/run.rs @@ -0,0 +1,10 @@ +//! Run `sqlclean` with the process arguments. + +use std::process::ExitCode; + +/// Parse the process arguments, run one subcommand and return its exit code. +pub fn run() -> ExitCode { + ExitCode::from(crate::command::_helpers::dispatch::run( + std::env::args_os().collect(), + )) +} diff --git a/crates/sqlclean-cli/src/command/mod.rs b/crates/sqlclean-cli/src/command/mod.rs new file mode 100644 index 0000000..0225000 --- /dev/null +++ b/crates/sqlclean-cli/src/command/mod.rs @@ -0,0 +1,6 @@ +//! The `sqlclean` command line: arguments, subcommands and exit codes. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; diff --git a/crates/sqlclean-cli/src/command/models.rs b/crates/sqlclean-cli/src/command/models.rs new file mode 100644 index 0000000..1b9df67 --- /dev/null +++ b/crates/sqlclean-cli/src/command/models.rs @@ -0,0 +1,94 @@ +//! The command-line interface. + +use std::path::PathBuf; + +use clap::{Args, Parser, Subcommand}; + +use crate::reporting::models::OutputFormat; + +/// Fast, opinionated SQL linting and deterministic formatting. +#[derive(Debug, Parser)] +#[command(name = "sqlclean", version, about)] +pub struct Cli { + /// Use this configuration file instead of the nearest sqlclean.toml. + #[arg(long, global = true)] + pub config: Option, + #[command(subcommand)] + pub command: Command, +} + +#[derive(Debug, Subcommand)] +pub enum Command { + /// Report Rule findings without changing any file. + Check(CheckArgs), + /// Apply Rule fixes until stable, then format. + Fix(FixArgs), + /// Rewrite SQL files in the canonical layout. + Format(FormatArgs), + /// List the available Rules, or show one. + Rules(RulesArgs), + /// Print the structural facts custom Rules read for SQL on standard input, as JSON. + Facts(FactsArgs), +} + +/// Rule selection shared by check and fix. +#[derive(Debug, Args)] +pub struct SelectionArgs { + /// Files or directories to read; the configuration root when omitted. + pub paths: Vec, + /// Rule codes or families to run instead of the configured selection. + #[arg(long, value_delimiter = ',')] + pub select: Option>, + /// Rule codes or families to skip, in addition to the configured ones. + #[arg(long, value_delimiter = ',')] + pub ignore: Vec, + /// Evaluate every file instead of reusing results for unchanged inputs. + #[arg(long)] + pub no_cache: bool, +} + +#[derive(Debug, Args)] +pub struct CheckArgs { + #[command(flatten)] + pub selection: SelectionArgs, + #[arg(long, value_enum, default_value = "text")] + pub output_format: OutputFormat, +} + +#[derive(Debug, Args)] +pub struct FixArgs { + #[command(flatten)] + pub selection: SelectionArgs, + /// Also apply fixes their Rules mark unsafe because they can change results. + #[arg(long)] + pub unsafe_fixes: bool, + /// Print the changes as a unified diff instead of writing them. + #[arg(long)] + pub diff: bool, +} + +#[derive(Debug, Args)] +pub struct FormatArgs { + /// Files or directories to format; the configuration root when omitted. + pub paths: Vec, + /// Fail when a file needs formatting, without writing it. + #[arg(long)] + pub check: bool, + /// Print the changes as a unified diff instead of writing them. + #[arg(long)] + pub diff: bool, +} + +#[derive(Debug, Args)] +pub struct RulesArgs { + /// Show one Rule's details. + pub code: Option, + #[arg(long)] + pub json: bool, +} + +#[derive(Debug, Args)] +pub struct FactsArgs { + #[arg(long, default_value = "generic")] + pub dialect: String, +} diff --git a/crates/sqlclean-cli/src/config/_helpers/file_schema.rs b/crates/sqlclean-cli/src/config/_helpers/file_schema.rs new file mode 100644 index 0000000..383c875 --- /dev/null +++ b/crates/sqlclean-cli/src/config/_helpers/file_schema.rs @@ -0,0 +1,46 @@ +//! The configuration file as written, before defaults and validation. + +use std::collections::BTreeMap; + +use serde::Deserialize; + +use crate::config::models::{DialectOverride, RuleException, RuleIgnore, Thresholds}; + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct ConfigFile { + pub(crate) dialect: Option, + pub(crate) include: Option>, + #[serde(default)] + pub(crate) exclude: Vec, + #[serde(default)] + pub(crate) dialects: Vec, + #[serde(default)] + pub(crate) format: FormatTable, + #[serde(default)] + pub(crate) rules: RulesTable, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct FormatTable { + pub(crate) line_width: Option, +} + +#[derive(Debug, Default, Deserialize)] +#[serde(deny_unknown_fields)] +pub(crate) struct RulesTable { + pub(crate) select: Option>, + #[serde(default)] + pub(crate) ignore: Vec, + #[serde(default)] + pub(crate) custom: Vec, + #[serde(default)] + pub(crate) thresholds: Thresholds, + #[serde(default)] + pub(crate) rule_options: BTreeMap, + #[serde(default)] + pub(crate) rule_exceptions: Vec, + #[serde(default)] + pub(crate) rule_ignores: Vec, +} diff --git a/crates/sqlclean-cli/src/config/_helpers/loading.rs b/crates/sqlclean-cli/src/config/_helpers/loading.rs new file mode 100644 index 0000000..a3624a5 --- /dev/null +++ b/crates/sqlclean-cli/src/config/_helpers/loading.rs @@ -0,0 +1,64 @@ +//! Locating and reading the configuration file. + +use std::fs; +use std::path::{Path, PathBuf}; + +use crate::config::_helpers::file_schema::ConfigFile; +use crate::config::_helpers::validation::validate; +use crate::config::constants::{ + CONFIG_FILE_NAME, DEFAULT_DIALECT, DEFAULT_INCLUDE, DEFAULT_LINE_WIDTH, +}; +use crate::config::models::{Config, RulesConfig}; + +pub(crate) fn load_config(explicit: Option<&Path>, start: &Path) -> Result { + let located: Option = match explicit { + Some(path) => Some(path.to_path_buf()), + None => find_config(start), + }; + let (root, file): (PathBuf, ConfigFile) = match located { + Some(path) => ( + path.parent() + .map_or_else(|| start.to_path_buf(), Path::to_path_buf), + read_config(&path)?, + ), + None => (start.to_path_buf(), ConfigFile::default()), + }; + let config: Config = effective_config(root, file); + validate(&config).map_err(|error| format!("{CONFIG_FILE_NAME}: {error}"))?; + Ok(config) +} + +fn find_config(start: &Path) -> Option { + start + .ancestors() + .map(|directory| directory.join(CONFIG_FILE_NAME)) + .find(|candidate| candidate.is_file()) +} + +fn read_config(path: &Path) -> Result { + let text: String = fs::read_to_string(path) + .map_err(|error| format!("could not read {}: {error}", path.display()))?; + toml::from_str(&text).map_err(|error| format!("{}: {error}", path.display())) +} + +fn effective_config(root: PathBuf, file: ConfigFile) -> Config { + Config { + root, + dialect: file.dialect.unwrap_or_else(|| DEFAULT_DIALECT.to_owned()), + include: file + .include + .unwrap_or_else(|| vec![DEFAULT_INCLUDE.to_owned()]), + exclude: file.exclude, + dialects: file.dialects, + line_width: file.format.line_width.unwrap_or(DEFAULT_LINE_WIDTH), + rules: RulesConfig { + select: file.rules.select, + ignore: file.rules.ignore, + custom: file.rules.custom, + thresholds: file.rules.thresholds, + rule_options: file.rules.rule_options, + rule_exceptions: file.rules.rule_exceptions, + rule_ignores: file.rules.rule_ignores, + }, + } +} diff --git a/crates/sqlclean-cli/src/config/_helpers/mod.rs b/crates/sqlclean-cli/src/config/_helpers/mod.rs new file mode 100644 index 0000000..bc14e27 --- /dev/null +++ b/crates/sqlclean-cli/src/config/_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod file_schema; +pub mod loading; +pub mod validation; diff --git a/crates/sqlclean-cli/src/config/_helpers/validation.rs b/crates/sqlclean-cli/src/config/_helpers/validation.rs new file mode 100644 index 0000000..7a72768 --- /dev/null +++ b/crates/sqlclean-cli/src/config/_helpers/validation.rs @@ -0,0 +1,56 @@ +//! Checks that need more than the schema: positive limits, valid globs, dialects and reasons. + +use globset::Glob; +use sqlclean::sql_lint::main::validate_dialect::validate_dialect; + +use crate::config::models::Config; + +pub(crate) fn validate(config: &Config) -> Result<(), String> { + validate_dialect(&config.dialect)?; + for dialect in &config.dialects { + validate_dialect(&dialect.dialect)?; + validate_globs(&dialect.paths)?; + } + validate_globs(&config.include)?; + validate_globs(&config.exclude)?; + if config.line_width == 0 { + return Err("[format] line_width must be positive".to_owned()); + } + let thresholds = config.rules.thresholds; + if thresholds.max_literal_length == 0 || thresholds.max_ranking_order_by == 0 { + return Err( + "[rules.thresholds] max_literal_length and max_ranking_order_by must be positive" + .to_owned(), + ); + } + for exception in &config.rules.rule_exceptions { + require_reason(&exception.reason, &exception.rule)?; + if exception.relation.is_none() && exception.cte.is_some() { + return Err(format!( + "rule exception for {} at {} sets cte without relation", + exception.rule, exception.path + )); + } + } + for ignore in &config.rules.rule_ignores { + require_reason(&ignore.reason, &ignore.rules.join(", "))?; + validate_globs(&ignore.paths)?; + } + Ok(()) +} + +fn validate_globs(patterns: &[String]) -> Result<(), String> { + for pattern in patterns { + Glob::new(pattern).map_err(|error| format!("invalid path pattern '{pattern}': {error}"))?; + } + Ok(()) +} + +fn require_reason(reason: &str, subject: &str) -> Result<(), String> { + if reason.trim().is_empty() { + return Err(format!( + "rule exception or ignore for {subject} needs a reason" + )); + } + Ok(()) +} diff --git a/crates/sqlclean-cli/src/config/constants.rs b/crates/sqlclean-cli/src/config/constants.rs new file mode 100644 index 0000000..692df56 --- /dev/null +++ b/crates/sqlclean-cli/src/config/constants.rs @@ -0,0 +1,6 @@ +pub const CONFIG_FILE_NAME: &str = "sqlclean.toml"; +pub(crate) const DEFAULT_DIALECT: &str = "generic"; +pub(crate) const DEFAULT_INCLUDE: &str = "**/*.sql"; +pub(crate) const DEFAULT_LINE_WIDTH: usize = 100; +pub(crate) const DEFAULT_MAX_LITERAL_LENGTH: usize = 100; +pub(crate) const DEFAULT_MAX_RANKING_ORDER_BY: usize = 5; diff --git a/crates/sqlclean-cli/src/config/main/load_config.rs b/crates/sqlclean-cli/src/config/main/load_config.rs new file mode 100644 index 0000000..305c78a --- /dev/null +++ b/crates/sqlclean-cli/src/config/main/load_config.rs @@ -0,0 +1,10 @@ +//! Find, read and validate the configuration for a command. + +use std::path::Path; + +use crate::config::models::Config; + +/// The configuration at `explicit`, else the nearest `sqlclean.toml` at or above `start`. +pub fn load_config(explicit: Option<&Path>, start: &Path) -> Result { + crate::config::_helpers::loading::load_config(explicit, start) +} diff --git a/crates/sqlclean-cli/src/config/main/mod.rs b/crates/sqlclean-cli/src/config/main/mod.rs new file mode 100644 index 0000000..1384d72 --- /dev/null +++ b/crates/sqlclean-cli/src/config/main/mod.rs @@ -0,0 +1 @@ +pub mod load_config; diff --git a/crates/sqlclean-cli/src/config/mod.rs b/crates/sqlclean-cli/src/config/mod.rs new file mode 100644 index 0000000..a68d4fb --- /dev/null +++ b/crates/sqlclean-cli/src/config/mod.rs @@ -0,0 +1,9 @@ +//! `sqlclean.toml`: where it is found, what it may contain and its defaults. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean-cli/src/config/models.rs b/crates/sqlclean-cli/src/config/models.rs new file mode 100644 index 0000000..e44b772 --- /dev/null +++ b/crates/sqlclean-cli/src/config/models.rs @@ -0,0 +1,80 @@ +//! The validated configuration every command reads. + +use std::collections::BTreeMap; +use std::path::PathBuf; + +use serde::Deserialize; + +/// The effective configuration: file values over defaults, with paths relative to `root`. +#[derive(Debug, Clone)] +pub struct Config { + /// The directory that holds the configuration file, or the working directory without one. + pub root: PathBuf, + pub dialect: String, + pub include: Vec, + pub exclude: Vec, + pub dialects: Vec, + pub line_width: usize, + pub rules: RulesConfig, +} + +/// A dialect for every file whose root-relative path matches one of `paths`. +#[derive(Debug, Clone, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct DialectOverride { + pub paths: Vec, + pub dialect: String, +} + +/// The `[rules]` table. +#[derive(Debug, Clone, Default)] +pub struct RulesConfig { + /// Exact codes or family prefixes; `None` selects the default Rules. + pub select: Option>, + pub ignore: Vec, + /// Python files or directories, relative to the root, that define custom Rules. + pub custom: Vec, + pub thresholds: Thresholds, + pub rule_options: BTreeMap, + pub rule_exceptions: Vec, + pub rule_ignores: Vec, +} + +/// Project-wide limits of the SQL quality Rules. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(default, deny_unknown_fields)] +pub struct Thresholds { + pub max_literal_length: usize, + pub max_ranking_order_by: usize, +} + +impl Default for Thresholds { + fn default() -> Self { + Self { + max_literal_length: crate::config::constants::DEFAULT_MAX_LITERAL_LENGTH, + max_ranking_order_by: crate::config::constants::DEFAULT_MAX_RANKING_ORDER_BY, + } + } +} + +/// One reviewed finding that is allowed: a Rule code in one file, optionally one joined relation. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RuleException { + pub rule: String, + pub path: String, + #[serde(default)] + pub relation: Option, + #[serde(default)] + pub cte: Option, + pub reason: String, +} + +/// Rule codes or families that do not apply to files matching `paths`. +#[derive(Debug, Clone, PartialEq, Eq, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct RuleIgnore { + pub rules: Vec, + pub paths: Vec, + pub reason: String, +} diff --git a/crates/sqlclean-cli/src/config/tests.rs b/crates/sqlclean-cli/src/config/tests.rs new file mode 100644 index 0000000..63f578b --- /dev/null +++ b/crates/sqlclean-cli/src/config/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_load_config.rs"] +mod load_config; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean-cli/src/config/tests/test_load_config.rs b/crates/sqlclean-cli/src/config/tests/test_load_config.rs new file mode 100644 index 0000000..7fdb954 --- /dev/null +++ b/crates/sqlclean-cli/src/config/tests/test_load_config.rs @@ -0,0 +1,118 @@ +use std::fs; + +use crate::config::main::load_config::load_config; +use crate::config::tests::test_types::{LoadedConfigTestCase, RejectedConfigTestCase}; + +#[test] +fn given_a_config_file_when_loading_from_below_then_the_nearest_one_applies() -> Result<(), String> +{ + let test_cases = [ + LoadedConfigTestCase { + description: "values set in a parent directory apply and unset thresholds keep defaults", + config_text: "dialect = \"duckdb\"\n[format]\nline_width = 80\n[rules]\nselect = [\"SQC004\"]\n[rules.thresholds]\nmax_literal_length = 40\n", + start: "models/orders", + expected_dialect: "duckdb", + expected_line_width: 80, + expected_select: Some(&["SQC004"]), + expected_max_literal_length: 40, + expected_max_ranking_order_by: 5, + }, + LoadedConfigTestCase { + description: "an empty file gives the defaults", + config_text: "", + start: "", + expected_dialect: "generic", + expected_line_width: 100, + expected_select: None, + expected_max_literal_length: 100, + expected_max_ranking_order_by: 5, + }, + ]; + for test_case in &test_cases { + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + let start = directory.path().join(test_case.start); + fs::create_dir_all(&start).map_err(|error| error.to_string())?; + fs::write( + directory.path().join("sqlclean.toml"), + test_case.config_text, + ) + .map_err(|error| error.to_string())?; + let config = load_config(None, &start)?; + let select: Option> = config + .rules + .select + .as_ref() + .map(|codes| codes.iter().map(String::as_str).collect()); + assert_eq!(config.root, directory.path(), "{}", test_case.description); + assert_eq!( + config.dialect, test_case.expected_dialect, + "{}", + test_case.description + ); + assert_eq!( + config.line_width, test_case.expected_line_width, + "{}", + test_case.description + ); + assert_eq!( + select, + test_case.expected_select.map(<[&str]>::to_vec), + "{}", + test_case.description + ); + assert_eq!( + config.rules.thresholds.max_literal_length, test_case.expected_max_literal_length, + "{}", + test_case.description + ); + assert_eq!( + config.rules.thresholds.max_ranking_order_by, test_case.expected_max_ranking_order_by, + "{}", + test_case.description + ); + } + Ok(()) +} + +#[test] +fn given_invalid_values_when_loading_then_each_is_rejected_with_its_reason() -> Result<(), String> { + let test_cases = [ + RejectedConfigTestCase { + description: "an unknown dialect", + config_text: "dialect = \"nosuch\"\n", + expected_error: "unknown SQL dialect 'nosuch'", + }, + RejectedConfigTestCase { + description: "an unknown rules key", + config_text: "[rules]\nunknown = 1\n", + expected_error: "unknown field `unknown`", + }, + RejectedConfigTestCase { + description: "an exception with a blank reason", + config_text: "[[rules.rule_exceptions]]\nrule = \"SQC004\"\npath = \"a.sql\"\nreason = \" \"\n", + expected_error: "needs a reason", + }, + RejectedConfigTestCase { + description: "a zero line width", + config_text: "[format]\nline_width = 0\n", + expected_error: "line_width must be positive", + }, + ]; + for test_case in &test_cases { + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + fs::write( + directory.path().join("sqlclean.toml"), + test_case.config_text, + ) + .map_err(|error| error.to_string())?; + let error: String = load_config(None, directory.path()) + .err() + .unwrap_or_default(); + assert!( + error.contains(test_case.expected_error), + "{}: {error}", + test_case.description + ); + } + Ok(()) +} diff --git a/crates/sqlclean-cli/src/config/tests/test_types.rs b/crates/sqlclean-cli/src/config/tests/test_types.rs new file mode 100644 index 0000000..1115689 --- /dev/null +++ b/crates/sqlclean-cli/src/config/tests/test_types.rs @@ -0,0 +1,16 @@ +pub(crate) struct LoadedConfigTestCase { + pub(crate) description: &'static str, + pub(crate) config_text: &'static str, + pub(crate) start: &'static str, + pub(crate) expected_dialect: &'static str, + pub(crate) expected_line_width: usize, + pub(crate) expected_select: Option<&'static [&'static str]>, + pub(crate) expected_max_literal_length: usize, + pub(crate) expected_max_ranking_order_by: usize, +} + +pub(crate) struct RejectedConfigTestCase { + pub(crate) description: &'static str, + pub(crate) config_text: &'static str, + pub(crate) expected_error: &'static str, +} diff --git a/crates/sqlclean-cli/src/discovery/_helpers/matching.rs b/crates/sqlclean-cli/src/discovery/_helpers/matching.rs new file mode 100644 index 0000000..d05d07b --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/_helpers/matching.rs @@ -0,0 +1,60 @@ +//! Root-relative path matching for include, exclude and per-path dialect patterns. + +use globset::{Glob, GlobSet, GlobSetBuilder}; + +use crate::config::models::Config; + +/// Compiled path patterns of one configuration. +pub(crate) struct PathMatchers { + include: GlobSet, + exclude: GlobSet, + dialects: Vec<(GlobSet, String)>, + default_dialect: String, +} + +impl PathMatchers { + pub(crate) fn new(config: &Config) -> Result { + let mut dialects: Vec<(GlobSet, String)> = Vec::new(); + for entry in &config.dialects { + dialects.push((glob_set(&entry.paths)?, entry.dialect.clone())); + } + Ok(Self { + include: glob_set(&config.include)?, + exclude: glob_set(&config.exclude)?, + dialects, + default_dialect: config.dialect.clone(), + }) + } + + pub(crate) fn is_included(&self, path: &str) -> bool { + self.include.is_match(path) && !self.is_excluded(path) + } + + pub(crate) fn is_excluded(&self, path: &str) -> bool { + self.exclude.is_match(path) + } + + /// The dialect of the first matching per-path entry, or the configured default. + pub(crate) fn dialect(&self, path: &str) -> String { + self.dialects + .iter() + .find(|(patterns, _)| patterns.is_match(path)) + .map_or_else( + || self.default_dialect.clone(), + |(_, dialect)| dialect.clone(), + ) + } +} + +/// Compile root-relative glob patterns; `*` does not cross `/`, and `**` does. +pub(crate) fn glob_set(patterns: &[String]) -> Result { + let mut builder = GlobSetBuilder::new(); + for pattern in patterns { + let glob: Glob = globset::GlobBuilder::new(pattern) + .literal_separator(true) + .build() + .map_err(|error| format!("invalid path pattern '{pattern}': {error}"))?; + builder.add(glob); + } + builder.build().map_err(|error| error.to_string()) +} diff --git a/crates/sqlclean-cli/src/discovery/_helpers/mod.rs b/crates/sqlclean-cli/src/discovery/_helpers/mod.rs new file mode 100644 index 0000000..130be8f --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/_helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod matching; +pub mod walking; diff --git a/crates/sqlclean-cli/src/discovery/_helpers/walking.rs b/crates/sqlclean-cli/src/discovery/_helpers/walking.rs new file mode 100644 index 0000000..212893a --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/_helpers/walking.rs @@ -0,0 +1,77 @@ +//! Walk the requested paths, honouring ignore files, and read the selected SQL files. + +use std::collections::BTreeMap; +use std::fs; +use std::path::{Path, PathBuf}; + +use ignore::WalkBuilder; + +use crate::config::models::Config; +use crate::discovery::_helpers::matching::PathMatchers; +use crate::discovery::models::SourceFile; + +pub(crate) fn discover_files( + config: &Config, + paths: &[PathBuf], +) -> Result, String> { + let matchers = PathMatchers::new(config)?; + let roots: Vec = if paths.is_empty() { + vec![config.root.clone()] + } else { + paths.to_vec() + }; + let mut selected: BTreeMap = BTreeMap::new(); + for root in &roots { + if !root.exists() { + return Err(format!("{}: no such file or directory", root.display())); + } + if root.is_file() { + let relative: String = relative_path(&config.root, root); + if !matchers.is_excluded(&relative) { + selected.insert(relative, root.clone()); + } + continue; + } + for entry in WalkBuilder::new(root).build() { + let entry = entry.map_err(|error| error.to_string())?; + if !entry.file_type().is_some_and(|kind| kind.is_file()) { + continue; + } + let relative: String = relative_path(&config.root, entry.path()); + if matchers.is_included(&relative) { + selected.insert(relative, entry.path().to_path_buf()); + } + } + } + selected + .into_iter() + .map(|(path, absolute)| read_source(&matchers, path, absolute)) + .collect() +} + +fn read_source( + matchers: &PathMatchers, + path: String, + absolute: PathBuf, +) -> Result { + let text: String = + fs::read_to_string(&absolute).map_err(|error| format!("could not read {path}: {error}"))?; + Ok(SourceFile { + dialect: matchers.dialect(&path), + path, + absolute, + text, + }) +} + +/// `path` relative to `root` with `/` separators, or as given when it lies outside the root. +pub(crate) fn relative_path(root: &Path, path: &Path) -> String { + let absolute_root: PathBuf = root.canonicalize().unwrap_or_else(|_| root.to_path_buf()); + let absolute_path: PathBuf = path.canonicalize().unwrap_or_else(|_| path.to_path_buf()); + let relative: &Path = absolute_path.strip_prefix(&absolute_root).unwrap_or(path); + relative + .components() + .map(|component| component.as_os_str().to_string_lossy().into_owned()) + .collect::>() + .join("/") +} diff --git a/crates/sqlclean-cli/src/discovery/main/discover_files.rs b/crates/sqlclean-cli/src/discovery/main/discover_files.rs new file mode 100644 index 0000000..248ac4c --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/main/discover_files.rs @@ -0,0 +1,11 @@ +//! Read every SQL file the given paths select. + +use std::path::PathBuf; + +use crate::config::models::Config; +use crate::discovery::models::SourceFile; + +/// The configured SQL files under `paths` (the configuration root when empty), sorted by path. +pub fn discover_files(config: &Config, paths: &[PathBuf]) -> Result, String> { + crate::discovery::_helpers::walking::discover_files(config, paths) +} diff --git a/crates/sqlclean-cli/src/discovery/main/mod.rs b/crates/sqlclean-cli/src/discovery/main/mod.rs new file mode 100644 index 0000000..1841aef --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/main/mod.rs @@ -0,0 +1,2 @@ +pub mod discover_files; +pub mod path_patterns; diff --git a/crates/sqlclean-cli/src/discovery/main/path_patterns.rs b/crates/sqlclean-cli/src/discovery/main/path_patterns.rs new file mode 100644 index 0000000..0493b4a --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/main/path_patterns.rs @@ -0,0 +1,8 @@ +//! Root-relative glob patterns compiled the way every configured path list is matched. + +use globset::GlobSet; + +/// Compile `patterns`; `*` does not cross `/`, and `**` does. +pub fn path_patterns(patterns: &[String]) -> Result { + crate::discovery::_helpers::matching::glob_set(patterns) +} diff --git a/crates/sqlclean-cli/src/discovery/mod.rs b/crates/sqlclean-cli/src/discovery/mod.rs new file mode 100644 index 0000000..ecb90e4 --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/mod.rs @@ -0,0 +1,8 @@ +//! Which SQL files a command reads, and the dialect of each. + +pub mod _helpers; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean-cli/src/discovery/models.rs b/crates/sqlclean-cli/src/discovery/models.rs new file mode 100644 index 0000000..e4de420 --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/models.rs @@ -0,0 +1,11 @@ +use std::path::PathBuf; + +/// One SQL file a command reads. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SourceFile { + /// The path relative to the configuration root, with `/` separators. + pub path: String, + pub absolute: PathBuf, + pub text: String, + pub dialect: String, +} diff --git a/crates/sqlclean-cli/src/discovery/tests.rs b/crates/sqlclean-cli/src/discovery/tests.rs new file mode 100644 index 0000000..de9dcc6 --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_discover_files.rs"] +mod discover_files; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean-cli/src/discovery/tests/test_discover_files.rs b/crates/sqlclean-cli/src/discovery/tests/test_discover_files.rs new file mode 100644 index 0000000..bc14258 --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/tests/test_discover_files.rs @@ -0,0 +1,67 @@ +use std::fs; +use std::path::PathBuf; + +use crate::config::main::load_config::load_config; +use crate::discovery::main::discover_files::discover_files; +use crate::discovery::tests::test_types::DiscoveryTestCase; + +const PROJECT: [(&str, &str); 7] = [ + ( + "sqlclean.toml", + "dialect = \"duckdb\"\nexclude = [\"vendor/**\"]\n[[dialects]]\npaths = [\"warehouse/**\"]\ndialect = \"snowflake\"\n", + ), + (".gitignore", "build/\n"), + ("models/orders.sql", "SELECT 1"), + ("warehouse/customers.sql", "SELECT 2"), + ("vendor/third_party.sql", "SELECT 3"), + ("build/generated.sql", "SELECT 4"), + ("notes.txt", "not sql"), +]; + +#[test] +fn given_patterns_and_ignore_files_when_discovering_then_selects_files_and_dialects() +-> Result<(), String> { + let test_cases = [ + DiscoveryTestCase { + description: "a full walk skips excluded and git-ignored files and maps dialects by path", + explicit_paths: &[], + expected_files: &[ + ("models/orders.sql", "duckdb"), + ("warehouse/customers.sql", "snowflake"), + ], + }, + DiscoveryTestCase { + description: "an explicit file is read on its own", + explicit_paths: &["models/orders.sql"], + expected_files: &[("models/orders.sql", "duckdb")], + }, + DiscoveryTestCase { + description: "an explicit directory is walked with the same filters", + explicit_paths: &["warehouse"], + expected_files: &[("warehouse/customers.sql", "snowflake")], + }, + ]; + let directory = tempfile::tempdir().map_err(|error| error.to_string())?; + let root = directory.path(); + for (path, text) in PROJECT { + let file = root.join(path); + fs::create_dir_all(file.parent().unwrap_or(root)).map_err(|error| error.to_string())?; + fs::write(file, text).map_err(|error| error.to_string())?; + } + fs::create_dir_all(root.join(".git")).map_err(|error| error.to_string())?; + let config = load_config(None, root)?; + for test_case in &test_cases { + let paths: Vec = test_case + .explicit_paths + .iter() + .map(|path| root.join(path)) + .collect(); + let files = discover_files(&config, &paths)?; + let found: Vec<(&str, &str)> = files + .iter() + .map(|file| (file.path.as_str(), file.dialect.as_str())) + .collect(); + assert_eq!(found, test_case.expected_files, "{}", test_case.description); + } + Ok(()) +} diff --git a/crates/sqlclean-cli/src/discovery/tests/test_types.rs b/crates/sqlclean-cli/src/discovery/tests/test_types.rs new file mode 100644 index 0000000..532cfae --- /dev/null +++ b/crates/sqlclean-cli/src/discovery/tests/test_types.rs @@ -0,0 +1,5 @@ +pub(crate) struct DiscoveryTestCase { + pub(crate) description: &'static str, + pub(crate) explicit_paths: &'static [&'static str], + pub(crate) expected_files: &'static [(&'static str, &'static str)], +} diff --git a/crates/sqlclean-cli/src/fixing/_helpers/edit_selection.rs b/crates/sqlclean-cli/src/fixing/_helpers/edit_selection.rs new file mode 100644 index 0000000..5aa8dad --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/_helpers/edit_selection.rs @@ -0,0 +1,56 @@ +//! Choose the fixes one pass can apply together. + +use std::collections::BTreeMap; + +use crate::checking::models::{Finding, FixSafety}; +use crate::positions::models::Edit; + +/// Edits by path from every eligible finding whose edits overlap no earlier accepted edit. +pub(crate) fn select_edits( + findings: &[Finding], + unsafe_fixes: bool, +) -> BTreeMap> { + let mut eligible: Vec<&Finding> = findings + .iter() + .filter(|finding| is_eligible(finding, unsafe_fixes)) + .collect(); + eligible.sort_by_key(|finding| (finding.path.clone(), finding.start, finding.code.clone())); + let mut accepted: BTreeMap> = BTreeMap::new(); + for finding in eligible { + let edits: &mut Vec = accepted.entry(finding.path.clone()).or_default(); + if is_compatible(&finding.edits, edits) { + edits.extend(finding.edits.iter().cloned()); + } + } + for edits in accepted.values_mut() { + edits.sort_by_key(|edit| (edit.start, edit.end)); + } + accepted.retain(|_, edits| !edits.is_empty()); + accepted +} + +fn is_eligible(finding: &Finding, unsafe_fixes: bool) -> bool { + match finding.fix { + Some(FixSafety::Safe) => true, + Some(FixSafety::Unsafe) => unsafe_fixes, + None => false, + } +} + +/// The finding's edits overlap neither each other nor any accepted edit. +fn is_compatible(edits: &[Edit], accepted: &[Edit]) -> bool { + edits.iter().enumerate().all(|(index, edit)| { + !overlaps_any(edit, accepted) && !overlaps_any(edit, &edits[index + 1..]) + }) +} + +fn overlaps_any(edit: &Edit, others: &[Edit]) -> bool { + others.iter().any(|other| overlaps(edit, other)) +} + +/// Two edits touch the same characters, or both insert at the same point. +fn overlaps(left: &Edit, right: &Edit) -> bool { + let both_insert_here: bool = + left.start == left.end && right.start == right.end && left.start == right.start; + both_insert_here || (left.start < right.end && right.start < left.end) +} diff --git a/crates/sqlclean-cli/src/fixing/_helpers/mod.rs b/crates/sqlclean-cli/src/fixing/_helpers/mod.rs new file mode 100644 index 0000000..148233a --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/_helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod edit_selection; +pub mod passes; diff --git a/crates/sqlclean-cli/src/fixing/_helpers/passes.rs b/crates/sqlclean-cli/src/fixing/_helpers/passes.rs new file mode 100644 index 0000000..9efc78e --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/_helpers/passes.rs @@ -0,0 +1,74 @@ +//! The fix loop: check, apply non-overlapping fixes, keep only passes that still parse. + +use std::collections::BTreeMap; + +use sqlclean::sql_facts::main::file_facts::file_facts; + +use crate::checking::main::check_sources::check_sources; +use crate::checking::models::{CheckOptions, CheckOutcome}; +use crate::config::models::Config; +use crate::discovery::models::SourceFile; +use crate::fixing::_helpers::edit_selection::select_edits; +use crate::fixing::constants::MAX_FIX_PASSES; +use crate::fixing::models::FixOutcome; +use crate::formatting::main::format_source::format_source; +use crate::positions::main::apply_edits::apply_edits; +use crate::positions::models::Edit; + +pub(crate) fn fix_sources( + config: &Config, + mut files: Vec, + options: &CheckOptions, + unsafe_fixes: bool, +) -> Result { + let mut applied_fixes: usize = 0; + let mut passes: usize = 0; + while passes < MAX_FIX_PASSES { + let outcome: CheckOutcome = check_sources(config, &files, options)?; + let edits: BTreeMap> = select_edits(&outcome.findings, unsafe_fixes); + let applied: usize = apply_pass(&mut files, &edits); + if applied == 0 { + break; + } + applied_fixes += applied; + passes += 1; + } + let mut format_refusals: Vec<(String, String)> = Vec::new(); + for file in &mut files { + match format_source(&file.text, &file.dialect, config.line_width) { + Ok(formatted) => file.text = formatted, + Err(reason) => format_refusals.push((file.path.clone(), reason)), + } + } + let remaining: CheckOutcome = check_sources(config, &files, options)?; + Ok(FixOutcome { + files, + applied_fixes, + passes, + format_refusals, + remaining, + }) +} + +/// Apply each file's edits unless they turn parsing SQL into SQL that no longer parses. +fn apply_pass(files: &mut [SourceFile], edits: &BTreeMap>) -> usize { + let mut applied: usize = 0; + for file in files.iter_mut() { + let Some(file_edits) = edits.get(&file.path) else { + continue; + }; + let fixed: String = apply_edits(&file.text, file_edits); + if fixed == file.text { + continue; + } + if let (Ok(_), Err(_)) = ( + file_facts(&file.text, &file.dialect), + file_facts(&fixed, &file.dialect), + ) { + continue; + } + file.text = fixed; + applied += file_edits.len(); + } + applied +} diff --git a/crates/sqlclean-cli/src/fixing/constants.rs b/crates/sqlclean-cli/src/fixing/constants.rs new file mode 100644 index 0000000..51eddc9 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/constants.rs @@ -0,0 +1,2 @@ +/// The most fix passes one run applies; a Rule whose fix keeps re-triggering stops here. +pub const MAX_FIX_PASSES: usize = 10; diff --git a/crates/sqlclean-cli/src/fixing/main/fix_sources.rs b/crates/sqlclean-cli/src/fixing/main/fix_sources.rs new file mode 100644 index 0000000..9747681 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/main/fix_sources.rs @@ -0,0 +1,16 @@ +//! Fix and format SQL files in memory; the caller decides whether to write them. + +use crate::checking::models::CheckOptions; +use crate::config::models::Config; +use crate::discovery::models::SourceFile; +use crate::fixing::models::FixOutcome; + +/// Apply safe fixes (and unsafe ones when `unsafe_fixes`) until stable, then format. +pub fn fix_sources( + config: &Config, + files: Vec, + options: &CheckOptions, + unsafe_fixes: bool, +) -> Result { + crate::fixing::_helpers::passes::fix_sources(config, files, options, unsafe_fixes) +} diff --git a/crates/sqlclean-cli/src/fixing/main/mod.rs b/crates/sqlclean-cli/src/fixing/main/mod.rs new file mode 100644 index 0000000..bb4e9bf --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/main/mod.rs @@ -0,0 +1 @@ +pub mod fix_sources; diff --git a/crates/sqlclean-cli/src/fixing/mod.rs b/crates/sqlclean-cli/src/fixing/mod.rs new file mode 100644 index 0000000..bae8480 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/mod.rs @@ -0,0 +1,9 @@ +//! Apply Rule fixes until the files are stable, then format them. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean-cli/src/fixing/models.rs b/crates/sqlclean-cli/src/fixing/models.rs new file mode 100644 index 0000000..3858145 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/models.rs @@ -0,0 +1,14 @@ +use crate::checking::models::CheckOutcome; +use crate::discovery::models::SourceFile; + +/// The fixed and formatted files, and what remains to fix by hand. +#[derive(Debug, Clone)] +pub struct FixOutcome { + /// Every input file with its new text; unchanged files keep their text. + pub files: Vec, + pub applied_fixes: usize, + pub passes: usize, + /// Files the formatter refused, with its reason. + pub format_refusals: Vec<(String, String)>, + pub remaining: CheckOutcome, +} diff --git a/crates/sqlclean-cli/src/fixing/tests.rs b/crates/sqlclean-cli/src/fixing/tests.rs new file mode 100644 index 0000000..16208ca --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/tests.rs @@ -0,0 +1,6 @@ +#[path = "tests/test_edit_selection.rs"] +mod edit_selection; +#[path = "tests/helpers.rs"] +mod helpers; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean-cli/src/fixing/tests/helpers.rs b/crates/sqlclean-cli/src/fixing/tests/helpers.rs new file mode 100644 index 0000000..75df54d --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/tests/helpers.rs @@ -0,0 +1,27 @@ +use crate::checking::models::{Finding, FixSafety}; +use crate::positions::models::Edit; + +/// A finding in `q.sql` whose fix applies `edits`. +pub(crate) fn finding( + code: &str, + fix: Option, + edits: &[(usize, usize, &str)], +) -> Finding { + Finding { + code: code.to_owned(), + path: "q.sql".to_owned(), + start: edits.first().map_or(0, |edit| edit.0), + end: 0, + message: String::new(), + remediation: String::new(), + edits: edits + .iter() + .map(|(start, end, text)| Edit { + start: *start, + end: *end, + text: (*text).to_owned(), + }) + .collect(), + fix, + } +} diff --git a/crates/sqlclean-cli/src/fixing/tests/test_edit_selection.rs b/crates/sqlclean-cli/src/fixing/tests/test_edit_selection.rs new file mode 100644 index 0000000..5b9faa3 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/tests/test_edit_selection.rs @@ -0,0 +1,37 @@ +use crate::checking::models::{Finding, FixSafety}; +use crate::fixing::_helpers::edit_selection::select_edits; +use crate::fixing::tests::helpers::finding; +use crate::fixing::tests::test_types::EditSelectionTestCase; + +#[test] +fn given_overlapping_and_unsafe_fixes_when_selecting_then_keeps_first_eligible_non_overlapping() { + let findings: Vec = vec![ + finding("A", Some(FixSafety::Safe), &[(0, 5, "x")]), + finding("B", Some(FixSafety::Safe), &[(3, 8, "y")]), + finding("C", Some(FixSafety::Safe), &[(5, 5, "z"), (9, 10, "")]), + finding("D", Some(FixSafety::Unsafe), &[(20, 21, "u")]), + finding("E", None, &[(30, 31, "n")]), + finding("F", Some(FixSafety::Safe), &[(9, 9, "w")]), + finding("G", Some(FixSafety::Safe), &[(9, 9, "v")]), + ]; + let test_cases = [ + EditSelectionTestCase { + description: "safe only: overlaps, a second insert at one point and unfixable findings are skipped", + unsafe_fixes: false, + expected_spans: &[(0, 5), (5, 5), (9, 9), (9, 10)], + }, + EditSelectionTestCase { + description: "unsafe fixes are added when allowed", + unsafe_fixes: true, + expected_spans: &[(0, 5), (5, 5), (9, 9), (9, 10), (20, 21)], + }, + ]; + for test_case in &test_cases { + let selected = select_edits(&findings, test_case.unsafe_fixes); + let spans: Vec<(usize, usize)> = selected + .get("q.sql") + .map(|edits| edits.iter().map(|edit| (edit.start, edit.end)).collect()) + .unwrap_or_default(); + assert_eq!(spans, test_case.expected_spans, "{}", test_case.description); + } +} diff --git a/crates/sqlclean-cli/src/fixing/tests/test_types.rs b/crates/sqlclean-cli/src/fixing/tests/test_types.rs new file mode 100644 index 0000000..f19e298 --- /dev/null +++ b/crates/sqlclean-cli/src/fixing/tests/test_types.rs @@ -0,0 +1,5 @@ +pub(crate) struct EditSelectionTestCase { + pub(crate) description: &'static str, + pub(crate) unsafe_fixes: bool, + pub(crate) expected_spans: &'static [(usize, usize)], +} diff --git a/crates/sqlclean-cli/src/formatting/constants.rs b/crates/sqlclean-cli/src/formatting/constants.rs new file mode 100644 index 0000000..69e54fd --- /dev/null +++ b/crates/sqlclean-cli/src/formatting/constants.rs @@ -0,0 +1,2 @@ +pub(crate) const FORMAT_REFUSAL: &str = "the formatter could not format this SQL"; +pub(crate) const STATEMENT_SEPARATOR: &str = "\n\n"; diff --git a/crates/sqlclean-cli/src/formatting/main/format_source.rs b/crates/sqlclean-cli/src/formatting/main/format_source.rs new file mode 100644 index 0000000..52bb41b --- /dev/null +++ b/crates/sqlclean-cli/src/formatting/main/format_source.rs @@ -0,0 +1,54 @@ +//! Format one SQL file: each statement in canonical layout, separated by one blank line. + +use sqlclean::sql_facts::main::file_facts::file_facts; +use sqlclean::sql_lint::constants::LINT_API_VERSION; +use sqlclean::sql_lint::main::format_requests::format_requests; +use sqlclean::sql_lint::models::{FormatRequest, FormatResponse}; + +use crate::formatting::constants::{FORMAT_REFUSAL as REFUSAL, STATEMENT_SEPARATOR}; + +/// `text` in canonical layout ending in one newline, or why the formatter left it unchanged. +pub fn format_source(text: &str, dialect: &str, line_width: usize) -> Result { + let facts = file_facts(text, dialect)?; + let characters: Vec = text.chars().collect(); + let mut boundaries: Vec = facts + .statements + .iter() + .map(|statement| statement.terminated_end) + .collect(); + if let Some(last) = boundaries.last_mut() { + *last = characters.len(); + } + let mut chunks: Vec = Vec::new(); + let mut start: usize = 0; + for end in boundaries { + let chunk: String = characters[start..end].iter().collect(); + start = end; + let formatted: String = format_statement(chunk.trim(), dialect, line_width)?; + if !formatted.trim().is_empty() { + chunks.push(formatted.trim_end().to_owned()); + } + } + if chunks.is_empty() { + return Ok(text.trim().to_owned()); + } + Ok(chunks.join(STATEMENT_SEPARATOR) + "\n") +} + +fn format_statement(sql: &str, dialect: &str, line_width: usize) -> Result { + let request = FormatRequest { + version: LINT_API_VERSION, + sql: sql.to_owned(), + dialect: dialect.to_owned(), + line_width: Some(line_width), + ..FormatRequest::default() + }; + let response: FormatResponse = format_requests(vec![request]) + .pop() + .ok_or_else(|| REFUSAL.to_owned())?; + if response.formatted { + Ok(response.sql) + } else { + Err(response.reason.unwrap_or_else(|| REFUSAL.to_owned())) + } +} diff --git a/crates/sqlclean-cli/src/formatting/main/mod.rs b/crates/sqlclean-cli/src/formatting/main/mod.rs new file mode 100644 index 0000000..caa197a --- /dev/null +++ b/crates/sqlclean-cli/src/formatting/main/mod.rs @@ -0,0 +1 @@ +pub mod format_source; diff --git a/crates/sqlclean-cli/src/formatting/mod.rs b/crates/sqlclean-cli/src/formatting/mod.rs new file mode 100644 index 0000000..641a36d --- /dev/null +++ b/crates/sqlclean-cli/src/formatting/mod.rs @@ -0,0 +1,4 @@ +//! Canonical layout of SQL text through the engine's formatter. + +pub mod constants; +pub mod main; diff --git a/crates/sqlclean-cli/src/hosting/_helpers/interpreter.rs b/crates/sqlclean-cli/src/hosting/_helpers/interpreter.rs new file mode 100644 index 0000000..5ace31e --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/_helpers/interpreter.rs @@ -0,0 +1,26 @@ +//! The Python interpreter that runs custom Rules. + +use std::env; +use std::path::PathBuf; + +use crate::hosting::constants::PYTHON_VARIABLE; + +const SIBLING_NAMES: [&str; 3] = ["python", "python3", "python.exe"]; +const PATH_FALLBACK: &str = "python3"; + +/// `SQLCLEAN_PYTHON`, else the interpreter installed beside this executable, else `python3`. +pub(crate) fn python_executable() -> PathBuf { + if let Some(value) = env::var_os(PYTHON_VARIABLE) { + return PathBuf::from(value); + } + let sibling: Option = match env::current_exe() { + Ok(current) => current.parent().and_then(|directory| { + SIBLING_NAMES + .iter() + .map(|name| directory.join(name)) + .find(|candidate| candidate.is_file()) + }), + Err(_) => None, + }; + sibling.unwrap_or_else(|| PathBuf::from(PATH_FALLBACK)) +} diff --git a/crates/sqlclean-cli/src/hosting/_helpers/mod.rs b/crates/sqlclean-cli/src/hosting/_helpers/mod.rs new file mode 100644 index 0000000..44f59d9 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/_helpers/mod.rs @@ -0,0 +1,2 @@ +pub mod interpreter; +pub mod process; diff --git a/crates/sqlclean-cli/src/hosting/_helpers/process.rs b/crates/sqlclean-cli/src/hosting/_helpers/process.rs new file mode 100644 index 0000000..0c60575 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/_helpers/process.rs @@ -0,0 +1,57 @@ +//! Start the host, write the request to its standard input and parse its standard output. + +use std::io::Write; +use std::process::{Command, Stdio}; + +use crate::hosting::_helpers::interpreter::python_executable; +use crate::hosting::constants::{HOST_PROGRAM, MISSING_PACKAGE_HELP}; +use crate::hosting::models::{HostRequest, HostResponse}; + +const MISSING_MODULE_MARKER: &str = "No module named 'sqlclean"; + +pub(crate) fn run_host(request: &HostRequest) -> Result { + let payload: Vec = serde_json::to_vec(request).map_err(|error| error.to_string())?; + let python = python_executable(); + let mut child = Command::new(&python) + .args(["-c", HOST_PROGRAM]) + .current_dir(&request.root) + .env("PYTHONHASHSEED", "0") + .env("PYTHONDONTWRITEBYTECODE", "1") + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .spawn() + .map_err(|error| { + format!( + "could not start {}: {error}. {MISSING_PACKAGE_HELP}", + python.display() + ) + })?; + if let Some(mut input) = child.stdin.take() { + input + .write_all(&payload) + .map_err(|error| format!("could not send custom Rules request: {error}"))?; + } + let output = child + .wait_with_output() + .map_err(|error| format!("custom Rules host failed: {error}"))?; + let stderr: String = String::from_utf8_lossy(&output.stderr).into_owned(); + if stderr.contains(MISSING_MODULE_MARKER) { + return Err(MISSING_PACKAGE_HELP.to_owned()); + } + let response: HostResponse = serde_json::from_slice(&output.stdout).map_err(|error| { + format!( + "custom Rules host returned no result ({error}):\n{}", + stderr.trim_end() + ) + })?; + match response.error { + Some(error) => Err(error), + None if !output.status.success() => Err(format!( + "custom Rules host exited with {}:\n{}", + output.status, + stderr.trim_end() + )), + None => Ok(response), + } +} diff --git a/crates/sqlclean-cli/src/hosting/constants.rs b/crates/sqlclean-cli/src/hosting/constants.rs new file mode 100644 index 0000000..b1efdf2 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/constants.rs @@ -0,0 +1,7 @@ +/// Names a Python interpreter to use instead of the one beside the `sqlclean` executable. +pub const PYTHON_VARIABLE: &str = "SQLCLEAN_PYTHON"; +pub(crate) const HOST_PROGRAM: &str = + "from sqlclean.hosting.main.serve import serve; raise SystemExit(serve())"; +pub(crate) const CATALOGUE_MODE: &str = "catalogue"; +pub(crate) const EVALUATE_MODE: &str = "evaluate"; +pub(crate) const MISSING_PACKAGE_HELP: &str = "Custom Rules need the sqlclean Python package in the environment that runs sqlclean: install it with `pip install sqlclean`, or set SQLCLEAN_PYTHON to an interpreter that has it."; diff --git a/crates/sqlclean-cli/src/hosting/main/mod.rs b/crates/sqlclean-cli/src/hosting/main/mod.rs new file mode 100644 index 0000000..fd108b6 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/main/mod.rs @@ -0,0 +1 @@ +pub mod run_host; diff --git a/crates/sqlclean-cli/src/hosting/main/run_host.rs b/crates/sqlclean-cli/src/hosting/main/run_host.rs new file mode 100644 index 0000000..7767003 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/main/run_host.rs @@ -0,0 +1,8 @@ +//! Send one request to a fresh Python host and read its response. + +use crate::hosting::models::{HostRequest, HostResponse}; + +/// Run the Python host for `request`; a host failure or reported error is an `Err`. +pub fn run_host(request: &HostRequest) -> Result { + crate::hosting::_helpers::process::run_host(request) +} diff --git a/crates/sqlclean-cli/src/hosting/mod.rs b/crates/sqlclean-cli/src/hosting/mod.rs new file mode 100644 index 0000000..d12a934 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/mod.rs @@ -0,0 +1,6 @@ +//! The Python process that imports and runs custom Rules, started only when one is needed. + +pub mod _helpers; +pub mod constants; +pub mod main; +pub mod models; diff --git a/crates/sqlclean-cli/src/hosting/models.rs b/crates/sqlclean-cli/src/hosting/models.rs new file mode 100644 index 0000000..9f00563 --- /dev/null +++ b/crates/sqlclean-cli/src/hosting/models.rs @@ -0,0 +1,59 @@ +//! The JSON exchanged with the Python host. + +use std::collections::BTreeMap; + +use serde::{Deserialize, Serialize}; +use sqlclean::sql_facts::models::FileFacts; + +use crate::checking::models::RuleInfo; +use crate::positions::models::Edit; + +/// One request: list the custom Rules, or evaluate the named ones. +#[derive(Debug, Serialize)] +pub struct HostRequest { + pub mode: String, + pub root: String, + pub custom: Vec, + pub options: BTreeMap, + pub files: Vec, + pub repository_rules: Vec, +} + +/// One file and the statement and file Rules to evaluate on it. +#[derive(Debug, Serialize)] +pub struct HostFile { + pub path: String, + pub source: String, + pub dialect: String, + pub facts: FileFacts, + pub rules: Vec, +} + +#[derive(Debug, Default, Deserialize)] +pub struct HostResponse { + #[serde(default)] + pub rules: Vec, + #[serde(default)] + pub results: Vec, + #[serde(default)] + pub error: Option, +} + +/// The findings of one Rule on one file, or on the repository when `path` is `None`. +#[derive(Debug, Deserialize)] +pub struct HostResult { + pub code: String, + pub path: Option, + pub findings: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HostFinding { + pub path: String, + pub start: usize, + pub end: usize, + pub message: String, + pub remediation: String, + #[serde(default)] + pub edits: Vec, +} diff --git a/crates/sqlclean-cli/src/lib.rs b/crates/sqlclean-cli/src/lib.rs new file mode 100644 index 0000000..13589f9 --- /dev/null +++ b/crates/sqlclean-cli/src/lib.rs @@ -0,0 +1,14 @@ +//! The `sqlclean` command: configuration, discovery, checking, fixing, formatting and reports. + +#![forbid(unsafe_code)] + +pub mod caching; +pub mod checking; +pub mod command; +pub mod config; +pub mod discovery; +pub mod fixing; +pub mod formatting; +pub mod hosting; +pub mod positions; +pub mod reporting; diff --git a/crates/sqlclean-cli/src/main.rs b/crates/sqlclean-cli/src/main.rs new file mode 100644 index 0000000..6f20307 --- /dev/null +++ b/crates/sqlclean-cli/src/main.rs @@ -0,0 +1,9 @@ +//! The `sqlclean` executable. + +use std::process::ExitCode; + +use sqlclean_cli::command::main::run::run; + +fn main() -> ExitCode { + run() +} diff --git a/crates/sqlclean-cli/src/positions/main/apply_edits.rs b/crates/sqlclean-cli/src/positions/main/apply_edits.rs new file mode 100644 index 0000000..5cebdce --- /dev/null +++ b/crates/sqlclean-cli/src/positions/main/apply_edits.rs @@ -0,0 +1,18 @@ +//! Apply non-overlapping character-offset edits to a text. + +use crate::positions::models::Edit; + +/// `text` with every edit applied; `edits` must be sorted by start and must not overlap. +pub fn apply_edits(text: &str, edits: &[Edit]) -> String { + let characters: Vec = text.chars().collect(); + let mut result = String::with_capacity(text.len()); + let mut cursor: usize = 0; + for edit in edits { + let start: usize = edit.start.min(characters.len()).max(cursor); + result.extend(&characters[cursor..start]); + result.push_str(&edit.text); + cursor = edit.end.min(characters.len()).max(start); + } + result.extend(&characters[cursor..]); + result +} diff --git a/crates/sqlclean-cli/src/positions/main/mod.rs b/crates/sqlclean-cli/src/positions/main/mod.rs new file mode 100644 index 0000000..2f22719 --- /dev/null +++ b/crates/sqlclean-cli/src/positions/main/mod.rs @@ -0,0 +1 @@ +pub mod apply_edits; diff --git a/crates/sqlclean-cli/src/positions/mod.rs b/crates/sqlclean-cli/src/positions/mod.rs new file mode 100644 index 0000000..6f6d393 --- /dev/null +++ b/crates/sqlclean-cli/src/positions/mod.rs @@ -0,0 +1,7 @@ +//! Character offsets: their line and column, and edits applied at them. + +pub mod main; +pub mod models; + +#[cfg(test)] +mod tests; diff --git a/crates/sqlclean-cli/src/positions/models.rs b/crates/sqlclean-cli/src/positions/models.rs new file mode 100644 index 0000000..0381d36 --- /dev/null +++ b/crates/sqlclean-cli/src/positions/models.rs @@ -0,0 +1,37 @@ +//! Offsets count Unicode characters, the unit of the engine's spans and of Python strings. + +/// The character offset at which each line of one text starts. +#[derive(Debug, Clone)] +pub struct LineIndex { + starts: Vec, + length: usize, +} + +impl LineIndex { + pub fn new(text: &str) -> Self { + let mut starts: Vec = vec![0]; + let mut length: usize = 0; + for (index, character) in text.chars().enumerate() { + if character == '\n' { + starts.push(index + 1); + } + length = index + 1; + } + Self { starts, length } + } + + /// The 1-based line and column of character `offset`, clamped to the end of the text. + pub fn line_column(&self, offset: usize) -> (usize, usize) { + let offset: usize = offset.min(self.length); + let line: usize = self.starts.partition_point(|start| *start <= offset); + (line, offset - self.starts[line - 1] + 1) + } +} + +/// A replacement of the characters `start..end` with `text`. +#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] +pub struct Edit { + pub start: usize, + pub end: usize, + pub text: String, +} diff --git a/crates/sqlclean-cli/src/positions/tests.rs b/crates/sqlclean-cli/src/positions/tests.rs new file mode 100644 index 0000000..393a10c --- /dev/null +++ b/crates/sqlclean-cli/src/positions/tests.rs @@ -0,0 +1,4 @@ +#[path = "tests/test_positions.rs"] +mod positions; +#[path = "tests/test_types.rs"] +mod test_types; diff --git a/crates/sqlclean-cli/src/positions/tests/test_positions.rs b/crates/sqlclean-cli/src/positions/tests/test_positions.rs new file mode 100644 index 0000000..888fc5b --- /dev/null +++ b/crates/sqlclean-cli/src/positions/tests/test_positions.rs @@ -0,0 +1,78 @@ +use crate::positions::main::apply_edits::apply_edits; +use crate::positions::models::{Edit, LineIndex}; +use crate::positions::tests::test_types::{ApplyEditsTestCase, LineColumnTestCase}; + +#[test] +fn given_multibyte_text_when_locating_offsets_then_lines_and_columns_count_characters() { + let index = LineIndex::new("SELECT 'é'\nFROM t\n"); + let test_cases = [ + LineColumnTestCase { + description: "the first character", + offset: 0, + expected_line_column: (1, 1), + }, + LineColumnTestCase { + description: "the closing quote after a two-byte character", + offset: 9, + expected_line_column: (1, 10), + }, + LineColumnTestCase { + description: "the start of the second line", + offset: 11, + expected_line_column: (2, 1), + }, + LineColumnTestCase { + description: "the end of the second line", + offset: 16, + expected_line_column: (2, 6), + }, + LineColumnTestCase { + description: "an offset past the end clamps to the last line", + offset: 99, + expected_line_column: (3, 1), + }, + ]; + for test_case in &test_cases { + assert_eq!( + index.line_column(test_case.offset), + test_case.expected_line_column, + "{}", + test_case.description + ); + } +} + +#[test] +fn given_edits_when_applying_then_offsets_count_characters() { + let test_cases = [ + ApplyEditsTestCase { + description: "insert, replace and delete after a multibyte character", + text: "SELECT 'é' FROM", + edits: &[(0, 0, "-- x\n"), (8, 11, "'e'"), (12, 16, "")], + expected_text: "-- x\nSELECT 'e' ", + }, + ApplyEditsTestCase { + description: "no edits leaves the text unchanged", + text: "SELECT 'é'", + edits: &[], + expected_text: "SELECT 'é'", + }, + ]; + for test_case in &test_cases { + let edits: Vec = test_case + .edits + .iter() + .map(|(start, end, text)| Edit { + start: *start, + end: *end, + text: (*text).to_owned(), + }) + .collect(); + assert_eq!( + apply_edits(test_case.text, &edits), + test_case.expected_text, + "{}", + test_case.description + ); + } +} diff --git a/crates/sqlclean-cli/src/positions/tests/test_types.rs b/crates/sqlclean-cli/src/positions/tests/test_types.rs new file mode 100644 index 0000000..5750021 --- /dev/null +++ b/crates/sqlclean-cli/src/positions/tests/test_types.rs @@ -0,0 +1,12 @@ +pub(crate) struct LineColumnTestCase { + pub(crate) description: &'static str, + pub(crate) offset: usize, + pub(crate) expected_line_column: (usize, usize), +} + +pub(crate) struct ApplyEditsTestCase { + pub(crate) description: &'static str, + pub(crate) text: &'static str, + pub(crate) edits: &'static [(usize, usize, &'static str)], + pub(crate) expected_text: &'static str, +} diff --git a/crates/sqlclean-cli/src/reporting/_helpers/location.rs b/crates/sqlclean-cli/src/reporting/_helpers/location.rs new file mode 100644 index 0000000..312c359 --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/_helpers/location.rs @@ -0,0 +1,32 @@ +//! Character offsets to line and column positions. + +use std::collections::HashMap; + +use crate::checking::models::Finding; +use crate::positions::models::LineIndex; +use crate::reporting::models::LocatedFinding; + +pub(crate) fn locate(findings: &[Finding], texts: &HashMap) -> Vec { + let mut indexes: HashMap<&str, LineIndex> = HashMap::new(); + findings + .iter() + .map(|finding| { + let index: &LineIndex = indexes.entry(finding.path.as_str()).or_insert_with(|| { + LineIndex::new(texts.get(&finding.path).map_or("", String::as_str)) + }); + let (line, column) = index.line_column(finding.start); + let (end_line, end_column) = index.line_column(finding.end.max(finding.start)); + LocatedFinding { + code: finding.code.clone(), + path: finding.path.clone(), + line, + column, + end_line, + end_column, + message: finding.message.clone(), + remediation: finding.remediation.clone(), + fix: finding.fix, + } + }) + .collect() +} diff --git a/crates/sqlclean-cli/src/reporting/_helpers/machine.rs b/crates/sqlclean-cli/src/reporting/_helpers/machine.rs new file mode 100644 index 0000000..e071fd7 --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/_helpers/machine.rs @@ -0,0 +1,68 @@ +//! JSON and SARIF reports. + +use serde_json::{Value, json}; + +use crate::checking::models::CheckOutcome; +use crate::reporting::models::LocatedFinding; + +const SARIF_SCHEMA: &str = "https://json.schemastore.org/sarif-2.1.0.json"; +const SARIF_VERSION: &str = "2.1.0"; +const TOOL_NAME: &str = "sqlclean"; +const TOOL_URI: &str = "https://github.com/sqlstack-io/sqlclean"; + +pub(crate) fn render_json(outcome: &CheckOutcome, located: &[LocatedFinding]) -> String { + let report: Value = json!({ + "findings": located, + "stale_exceptions": outcome.stale_exceptions, + "summary": { + "findings": located.len(), + "fixable": located.iter().filter(|finding| finding.fix.is_some()).count(), + "stale_exceptions": outcome.stale_exceptions.len(), + }, + }); + format!("{report:#}\n") +} + +pub(crate) fn render_sarif(located: &[LocatedFinding]) -> String { + let mut codes: Vec<&str> = located + .iter() + .map(|finding| finding.code.as_str()) + .collect(); + codes.sort_unstable(); + codes.dedup(); + let results: Vec = located + .iter() + .map(|finding| { + json!({ + "ruleId": finding.code, + "level": "error", + "message": { "text": format!("{} {}", finding.message, finding.remediation) }, + "locations": [{ + "physicalLocation": { + "artifactLocation": { "uri": finding.path }, + "region": { + "startLine": finding.line, + "startColumn": finding.column, + "endLine": finding.end_line, + "endColumn": finding.end_column.max(finding.column + 1), + }, + }, + }], + }) + }) + .collect(); + let report: Value = json!({ + "$schema": SARIF_SCHEMA, + "version": SARIF_VERSION, + "runs": [{ + "tool": { "driver": { + "name": TOOL_NAME, + "informationUri": TOOL_URI, + "version": env!("CARGO_PKG_VERSION"), + "rules": codes.iter().map(|code| json!({ "id": code })).collect::>(), + }}, + "results": results, + }], + }); + format!("{report:#}\n") +} diff --git a/crates/sqlclean-cli/src/reporting/_helpers/mod.rs b/crates/sqlclean-cli/src/reporting/_helpers/mod.rs new file mode 100644 index 0000000..60264ba --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/_helpers/mod.rs @@ -0,0 +1,3 @@ +pub mod location; +pub mod machine; +pub mod text; diff --git a/crates/sqlclean-cli/src/reporting/_helpers/text.rs b/crates/sqlclean-cli/src/reporting/_helpers/text.rs new file mode 100644 index 0000000..cf96bc2 --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/_helpers/text.rs @@ -0,0 +1,90 @@ +//! The terminal report: each finding with its source line, then a summary. + +use std::collections::HashMap; +use std::fmt::Write; + +use crate::checking::models::CheckOutcome; +use crate::config::constants::CONFIG_FILE_NAME; +use crate::reporting::models::LocatedFinding; + +pub(crate) fn render( + outcome: &CheckOutcome, + located: &[LocatedFinding], + texts: &HashMap, +) -> String { + let mut report = String::new(); + for finding in located { + report.push_str(&finding_text(finding, texts.get(&finding.path))); + } + for stale in &outcome.stale_exceptions { + let relation: String = stale + .relation + .as_ref() + .map(|relation| format!(" for relation {relation}")) + .unwrap_or_default(); + let _ = writeln!( + report, + "error: stale rule exception: {} in {}{relation} matches no finding; remove it from {CONFIG_FILE_NAME}\n", + stale.rule, stale.path + ); + } + let fixable: usize = located + .iter() + .filter(|finding| finding.fix.is_some()) + .count(); + let problems: usize = located.len() + outcome.stale_exceptions.len(); + if problems == 0 { + report.push_str("All checks passed.\n"); + } else { + let noun: &str = if problems == 1 { "problem" } else { "problems" }; + let _ = write!(report, "Found {problems} {noun}"); + if fixable > 0 { + let _ = write!(report, " ({fixable} fixable with `sqlclean fix`)"); + } + report.push_str(".\n"); + } + report +} + +fn finding_text(finding: &LocatedFinding, text: Option<&String>) -> String { + let mut report = String::new(); + let _ = writeln!(report, "error[{}]: {}", finding.code, finding.message); + let _ = writeln!( + report, + " --> {}:{}:{}", + finding.path, finding.line, finding.column + ); + if let Some(line) = text.and_then(|text| text.lines().nth(finding.line - 1)) { + let number: String = finding.line.to_string(); + let gutter: String = " ".repeat(number.len()); + let width: usize = if finding.end_line == finding.line { + finding.end_column.saturating_sub(finding.column).max(1) + } else { + line.chars() + .count() + .saturating_sub(finding.column - 1) + .max(1) + }; + let _ = writeln!(report, " {gutter} |"); + let _ = writeln!(report, " {number} | {line}"); + let _ = writeln!( + report, + " {gutter} | {}{}", + " ".repeat(finding.column - 1), + "^".repeat(width) + ); + } + for (index, help) in finding.remediation.lines().enumerate() { + let label: &str = if index == 0 { " = help: " } else { "" }; + let _ = writeln!(report, "{label}{help}"); + } + if let Some(fix) = finding.fix { + let flag: &str = match fix { + crate::checking::models::FixSafety::Safe => "", + crate::checking::models::FixSafety::Unsafe => " --unsafe-fixes", + }; + let _ = writeln!(report, " = fix: available with `sqlclean fix{flag}`"); + } + report.push('\n'); + report +} diff --git a/crates/sqlclean-cli/src/reporting/main/mod.rs b/crates/sqlclean-cli/src/reporting/main/mod.rs new file mode 100644 index 0000000..369023b --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/main/mod.rs @@ -0,0 +1 @@ +pub mod render_findings; diff --git a/crates/sqlclean-cli/src/reporting/main/render_findings.rs b/crates/sqlclean-cli/src/reporting/main/render_findings.rs new file mode 100644 index 0000000..00d9a85 --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/main/render_findings.rs @@ -0,0 +1,20 @@ +//! Render one check's outcome in the requested format. + +use std::collections::HashMap; + +use crate::checking::models::CheckOutcome; +use crate::reporting::models::OutputFormat; + +/// The report for `outcome`; `texts` holds each checked file's text by path. +pub fn render_findings( + outcome: &CheckOutcome, + texts: &HashMap, + format: OutputFormat, +) -> String { + let located = crate::reporting::_helpers::location::locate(&outcome.findings, texts); + match format { + OutputFormat::Text => crate::reporting::_helpers::text::render(outcome, &located, texts), + OutputFormat::Json => crate::reporting::_helpers::machine::render_json(outcome, &located), + OutputFormat::Sarif => crate::reporting::_helpers::machine::render_sarif(&located), + } +} diff --git a/crates/sqlclean-cli/src/reporting/mod.rs b/crates/sqlclean-cli/src/reporting/mod.rs new file mode 100644 index 0000000..9c7f2e7 --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/mod.rs @@ -0,0 +1,5 @@ +//! Findings rendered as text for people, JSON for tools and SARIF for code scanning. + +pub mod _helpers; +pub mod main; +pub mod models; diff --git a/crates/sqlclean-cli/src/reporting/models.rs b/crates/sqlclean-cli/src/reporting/models.rs new file mode 100644 index 0000000..315feee --- /dev/null +++ b/crates/sqlclean-cli/src/reporting/models.rs @@ -0,0 +1,24 @@ +use serde::Serialize; + +use crate::checking::models::FixSafety; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, clap::ValueEnum)] +pub enum OutputFormat { + Text, + Json, + Sarif, +} + +/// One finding with 1-based line and column positions, as JSON output writes it. +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub struct LocatedFinding { + pub code: String, + pub path: String, + pub line: usize, + pub column: usize, + pub end_line: usize, + pub end_column: usize, + pub message: String, + pub remediation: String, + pub fix: Option, +} From fe86c04da74e505f74a08ad791993b7f732d9e64 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:26 +0000 Subject: [PATCH 04/29] feat: add Python custom Rules, their host and test harness Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- src/sqlclean/__init__.py | 39 +++ src/sqlclean/hosting/__init__.py | 0 src/sqlclean/hosting/_helpers/__init__.py | 0 src/sqlclean/hosting/_helpers/evaluation.py | 109 ++++++++ src/sqlclean/hosting/_helpers/loading.py | 64 +++++ src/sqlclean/hosting/_helpers/requests.py | 116 +++++++++ src/sqlclean/hosting/_helpers/sql_files.py | 126 +++++++++ src/sqlclean/hosting/constants.py | 9 + src/sqlclean/hosting/exceptions.py | 11 + src/sqlclean/hosting/main/__init__.py | 0 src/sqlclean/hosting/main/build_sql_file.py | 12 + .../hosting/main/evaluate_definition.py | 17 ++ src/sqlclean/hosting/main/serve.py | 15 ++ src/sqlclean/py.typed | 0 src/sqlclean/rule_testing/__init__.py | 0 .../rule_testing/_helpers/__init__.py | 0 src/sqlclean/rule_testing/_helpers/harness.py | 66 +++++ .../rule_testing/_helpers/native_facts.py | 39 +++ src/sqlclean/rule_testing/constants.py | 6 + src/sqlclean/rule_testing/exceptions.py | 7 + src/sqlclean/rule_testing/main/__init__.py | 0 .../rule_testing/main/evaluate_rule.py | 11 + src/sqlclean/rule_testing/models.py | 42 +++ src/sqlclean/rules/__init__.py | 0 src/sqlclean/rules/_helpers/__init__.py | 0 src/sqlclean/rules/_helpers/definition.py | 107 ++++++++ src/sqlclean/rules/classes/__init__.py | 0 src/sqlclean/rules/classes/rule_context.py | 99 ++++++++ src/sqlclean/rules/classes/rule_option.py | 128 ++++++++++ src/sqlclean/rules/constants.py | 12 + src/sqlclean/rules/exceptions.py | 17 ++ src/sqlclean/rules/main/__init__.py | 0 src/sqlclean/rules/main/define.py | 30 +++ src/sqlclean/rules/models.py | 194 ++++++++++++++ src/sqlclean/rules/types.py | 33 +++ src/sqlclean/testing.py | 6 + tests/__init__.py | 0 tests/e2e/__init__.py | 0 tests/e2e/src/__init__.py | 0 tests/e2e/src/sqlclean/__init__.py | 0 tests/e2e/src/sqlclean/hosting/__init__.py | 0 tests/e2e/src/sqlclean/hosting/_test_types.py | 51 ++++ tests/e2e/src/sqlclean/hosting/helpers.py | 45 ++++ .../sqlclean/hosting/test_sqlclean_command.py | 239 ++++++++++++++++++ tests/integration/__init__.py | 0 tests/integration/src/__init__.py | 0 tests/integration/src/sqlclean/__init__.py | 0 .../src/sqlclean/rule_testing/__init__.py | 0 .../src/sqlclean/rule_testing/_test_types.py | 28 ++ .../src/sqlclean/rule_testing/helpers.py | 105 ++++++++ .../rule_testing/test_evaluate_rule.py | 149 +++++++++++ tests/unit/__init__.py | 0 tests/unit/src/__init__.py | 0 tests/unit/src/sqlclean/__init__.py | 0 tests/unit/src/sqlclean/rules/__init__.py | 0 tests/unit/src/sqlclean/rules/_test_types.py | 14 + tests/unit/src/sqlclean/rules/helpers.py | 12 + .../sqlclean/rules/test_rule_definition.py | 69 +++++ 58 files changed, 2027 insertions(+) create mode 100644 src/sqlclean/__init__.py create mode 100644 src/sqlclean/hosting/__init__.py create mode 100644 src/sqlclean/hosting/_helpers/__init__.py create mode 100644 src/sqlclean/hosting/_helpers/evaluation.py create mode 100644 src/sqlclean/hosting/_helpers/loading.py create mode 100644 src/sqlclean/hosting/_helpers/requests.py create mode 100644 src/sqlclean/hosting/_helpers/sql_files.py create mode 100644 src/sqlclean/hosting/constants.py create mode 100644 src/sqlclean/hosting/exceptions.py create mode 100644 src/sqlclean/hosting/main/__init__.py create mode 100644 src/sqlclean/hosting/main/build_sql_file.py create mode 100644 src/sqlclean/hosting/main/evaluate_definition.py create mode 100644 src/sqlclean/hosting/main/serve.py create mode 100644 src/sqlclean/py.typed create mode 100644 src/sqlclean/rule_testing/__init__.py create mode 100644 src/sqlclean/rule_testing/_helpers/__init__.py create mode 100644 src/sqlclean/rule_testing/_helpers/harness.py create mode 100644 src/sqlclean/rule_testing/_helpers/native_facts.py create mode 100644 src/sqlclean/rule_testing/constants.py create mode 100644 src/sqlclean/rule_testing/exceptions.py create mode 100644 src/sqlclean/rule_testing/main/__init__.py create mode 100644 src/sqlclean/rule_testing/main/evaluate_rule.py create mode 100644 src/sqlclean/rule_testing/models.py create mode 100644 src/sqlclean/rules/__init__.py create mode 100644 src/sqlclean/rules/_helpers/__init__.py create mode 100644 src/sqlclean/rules/_helpers/definition.py create mode 100644 src/sqlclean/rules/classes/__init__.py create mode 100644 src/sqlclean/rules/classes/rule_context.py create mode 100644 src/sqlclean/rules/classes/rule_option.py create mode 100644 src/sqlclean/rules/constants.py create mode 100644 src/sqlclean/rules/exceptions.py create mode 100644 src/sqlclean/rules/main/__init__.py create mode 100644 src/sqlclean/rules/main/define.py create mode 100644 src/sqlclean/rules/models.py create mode 100644 src/sqlclean/rules/types.py create mode 100644 src/sqlclean/testing.py create mode 100644 tests/__init__.py create mode 100644 tests/e2e/__init__.py create mode 100644 tests/e2e/src/__init__.py create mode 100644 tests/e2e/src/sqlclean/__init__.py create mode 100644 tests/e2e/src/sqlclean/hosting/__init__.py create mode 100644 tests/e2e/src/sqlclean/hosting/_test_types.py create mode 100644 tests/e2e/src/sqlclean/hosting/helpers.py create mode 100644 tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py create mode 100644 tests/integration/__init__.py create mode 100644 tests/integration/src/__init__.py create mode 100644 tests/integration/src/sqlclean/__init__.py create mode 100644 tests/integration/src/sqlclean/rule_testing/__init__.py create mode 100644 tests/integration/src/sqlclean/rule_testing/_test_types.py create mode 100644 tests/integration/src/sqlclean/rule_testing/helpers.py create mode 100644 tests/integration/src/sqlclean/rule_testing/test_evaluate_rule.py create mode 100644 tests/unit/__init__.py create mode 100644 tests/unit/src/__init__.py create mode 100644 tests/unit/src/sqlclean/__init__.py create mode 100644 tests/unit/src/sqlclean/rules/__init__.py create mode 100644 tests/unit/src/sqlclean/rules/_test_types.py create mode 100644 tests/unit/src/sqlclean/rules/helpers.py create mode 100644 tests/unit/src/sqlclean/rules/test_rule_definition.py diff --git a/src/sqlclean/__init__.py b/src/sqlclean/__init__.py new file mode 100644 index 0000000..c1193c0 --- /dev/null +++ b/src/sqlclean/__init__.py @@ -0,0 +1,39 @@ +"""SQLClean: SQL linting and deterministic formatting, with custom Rules in Python.""" + +from sqlclean.rules.classes.rule_context import RuleContext +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.main.define import rule +from sqlclean.rules.models import ( + ColumnReference, + Cte, + Edit, + Finding, + FunctionCall, + Join, + Literal, + Repository, + SqlFile, + Star, + Statement, + Table, +) +from sqlclean.rules.types import FixSafety + +__all__ = [ + "ColumnReference", + "Cte", + "Edit", + "Finding", + "FixSafety", + "FunctionCall", + "Join", + "Literal", + "Repository", + "RuleContext", + "RuleOption", + "SqlFile", + "Star", + "Statement", + "Table", + "rule", +] diff --git a/src/sqlclean/hosting/__init__.py b/src/sqlclean/hosting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/hosting/_helpers/__init__.py b/src/sqlclean/hosting/_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/hosting/_helpers/evaluation.py b/src/sqlclean/hosting/_helpers/evaluation.py new file mode 100644 index 0000000..26d4f3a --- /dev/null +++ b/src/sqlclean/hosting/_helpers/evaluation.py @@ -0,0 +1,109 @@ +"""Run one Rule over its subjects and check what it returns.""" + +import traceback +from collections.abc import Iterator, Mapping +from typing import cast + +from sqlclean.hosting.exceptions import RuleEvaluationError +from sqlclean.rules.classes.rule_context import RuleContext +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.exceptions import RuleOptionError, SqlCleanError +from sqlclean.rules.models import Finding, Repository, RuleDefinition, SqlFile +from sqlclean.rules.types import RuleOptionValue, RuleSubjectKind + + +def evaluate( + *, + definition: RuleDefinition, + files: tuple[SqlFile, ...], + options: Mapping[str, object], +) -> tuple[Finding, ...]: + """Return the Rule's findings over each of its subjects in files.""" + + values: dict[str, RuleOptionValue] = parse_options(definition=definition, configured=options) + sources: dict[str, str] = {file.path: file.source for file in files} + findings: list[Finding] = [] + for subject, file in _subjects(definition=definition, files=files): + context: RuleContext = RuleContext(definition=definition, options=values, file=file) + returned: object = _call(definition=definition, subject=subject, context=context) + findings.extend(_checked(definition=definition, returned=returned, sources=sources)) + return tuple(findings) + + +def parse_options( + *, definition: RuleDefinition, configured: Mapping[str, object] +) -> dict[str, RuleOptionValue]: + """Return the configured option values, validated against the Rule's declarations.""" + + declared: dict[str, RuleOption[object]] = {option.name: option for option in definition.options} + unknown: list[str] = sorted(set(configured) - set(declared)) + if unknown: + raise RuleOptionError(f"Rule {definition.code} has no option {', '.join(unknown)}") + values: dict[str, RuleOptionValue] = {} + for option in definition.options: + if option.name in configured: + try: + values[option.name] = cast(RuleOptionValue, option.parse(configured[option.name])) + except RuleOptionError as error: + raise RuleOptionError(f"[rules.rule_options.{definition.code}] {error}") from error + return values + + +def _subjects( + *, definition: RuleDefinition, files: tuple[SqlFile, ...] +) -> Iterator[tuple[object, SqlFile | None]]: + if definition.subject is RuleSubjectKind.REPOSITORY: + yield Repository(files=files), None + return + for file in files: + if definition.subject is RuleSubjectKind.FILE: + yield file, file + else: + for statement in file.statements: + yield statement, file + + +def _call(*, definition: RuleDefinition, subject: object, context: RuleContext) -> object: + arguments: dict[str, object] = { + definition.subject_parameter: subject, + definition.context_parameter: context, + } + try: + return definition.check(**arguments) + except SqlCleanError: + raise + except Exception as error: + location: str = getattr(subject, "path", "the repository") + raise RuleEvaluationError( + f"custom Rule {definition.code} failed on {location}: " + f"{type(error).__name__}: {error}\n{traceback.format_exc()}" + ) from error + + +def _checked( + *, definition: RuleDefinition, returned: object, sources: Mapping[str, str] +) -> list[Finding]: + if not isinstance(returned, list | tuple) or not all( + isinstance(item, Finding) for item in returned + ): + raise RuleEvaluationError( + f"custom Rule {definition.code} must return a list of findings from ctx.finding(...)" + ) + for finding in returned: + _check_finding(definition=definition, finding=finding, sources=sources) + return list(returned) + + +def _check_finding( + *, definition: RuleDefinition, finding: Finding, sources: Mapping[str, str] +) -> None: + source: str | None = sources.get(finding.path) + if finding.code != definition.code or source is None: + raise RuleEvaluationError( + f"custom Rule {definition.code} returned a finding for another Rule or file" + ) + for edit in finding.edits: + if not 0 <= edit.start <= edit.end <= len(source): + raise RuleEvaluationError( + f"custom Rule {definition.code} returned an edit outside {finding.path}" + ) diff --git a/src/sqlclean/hosting/_helpers/loading.py b/src/sqlclean/hosting/_helpers/loading.py new file mode 100644 index 0000000..425808b --- /dev/null +++ b/src/sqlclean/hosting/_helpers/loading.py @@ -0,0 +1,64 @@ +"""Import the configured custom Rule modules and collect their @rule functions.""" + +import sys +from importlib import import_module +from pathlib import Path +from types import ModuleType + +from sqlclean.hosting.constants import ( + PYTHON_SUFFIX, + SKIPPED_DIRECTORIES, + SKIPPED_MODULE_NAMES, + SKIPPED_MODULE_PREFIX, +) +from sqlclean.hosting.exceptions import RuleLoadingError +from sqlclean.rules.constants import RULE_ATTRIBUTE +from sqlclean.rules.models import RuleDefinition + + +def load_definitions(*, root: Path, custom: list[str]) -> dict[str, RuleDefinition]: + """Return every custom Rule defined in the modules under the configured paths, by code.""" + + root_text: str = str(root) + if root_text not in sys.path: + sys.path.insert(0, root_text) + definitions: dict[str, RuleDefinition] = {} + for module_name in _module_names(root=root, custom=custom): + module: ModuleType = _import(module_name) + for value in vars(module).values(): + definition: object = getattr(value, RULE_ATTRIBUTE, None) + if not isinstance(definition, RuleDefinition) or definition.module != module_name: + continue + if definition.code in definitions: + raise RuleLoadingError(f"custom Rule code {definition.code} is defined twice") + definitions[definition.code] = definition + return definitions + + +def _module_names(*, root: Path, custom: list[str]) -> list[str]: + files: set[Path] = set() + for entry in custom: + path: Path = root / entry + candidates: list[Path] = ( + [path] if path.is_file() else sorted(path.rglob(f"*{PYTHON_SUFFIX}")) + ) + files.update(candidate for candidate in candidates if _is_rule_module(candidate)) + return sorted(".".join(file.relative_to(root).with_suffix("").parts) for file in files) + + +def _is_rule_module(path: Path) -> bool: + return ( + path.suffix == PYTHON_SUFFIX + and path.name not in SKIPPED_MODULE_NAMES + and not path.name.startswith(SKIPPED_MODULE_PREFIX) + and not SKIPPED_DIRECTORIES.intersection(path.parts) + ) + + +def _import(module_name: str) -> ModuleType: + try: + return import_module(module_name) + except Exception as error: + raise RuleLoadingError( + f"could not import custom Rule module {module_name}: {type(error).__name__}: {error}" + ) from error diff --git a/src/sqlclean/hosting/_helpers/requests.py b/src/sqlclean/hosting/_helpers/requests.py new file mode 100644 index 0000000..c3bb10e --- /dev/null +++ b/src/sqlclean/hosting/_helpers/requests.py @@ -0,0 +1,116 @@ +"""One host request: list the custom Rules, or evaluate them over files.""" + +from collections.abc import Mapping, Sequence +from pathlib import Path + +from sqlclean.hosting._helpers.evaluation import evaluate +from sqlclean.hosting._helpers.loading import load_definitions +from sqlclean.hosting._helpers.sql_files import sql_file_from_facts +from sqlclean.hosting.constants import CATALOGUE_MODE +from sqlclean.hosting.exceptions import RuleEvaluationError +from sqlclean.rules.exceptions import SqlCleanError +from sqlclean.rules.models import Edit, Finding, RuleDefinition, SqlFile + +type _Json = dict[str, object] + + +def handle_request(request: Mapping[str, object]) -> _Json: + """Return the response to one request, or an error the command reports.""" + + try: + custom: list[str] = [str(path) for path in _list(container=request, key="custom")] + definitions: dict[str, RuleDefinition] = load_definitions( + root=Path(str(request["root"])), custom=custom + ) + if request["mode"] == CATALOGUE_MODE: + return {"rules": [_rule_info(definition) for definition in definitions.values()]} + return {"results": _evaluate(request=request, definitions=definitions)} + except SqlCleanError as error: + return {"error": str(error)} + + +def _evaluate( + *, request: Mapping[str, object], definitions: Mapping[str, RuleDefinition] +) -> list[_Json]: + files: list[tuple[SqlFile, list[str]]] = [ + _requested_file(item) for item in _list(container=request, key="files") + ] + options: Mapping[str, object] = _mapping(container=request, key="options") + results: list[_Json] = [] + for file, codes in files: + for code in codes: + findings: tuple[Finding, ...] = evaluate( + definition=_definition(definitions=definitions, code=code), + files=(file,), + options=_mapping(container=options, key=code), + ) + results.append(_result(code=code, path=file.path, findings=findings)) + every_file: tuple[SqlFile, ...] = tuple(file for file, _ in files) + for code in _list(container=request, key="repository_rules"): + findings = evaluate( + definition=_definition(definitions=definitions, code=str(code)), + files=every_file, + options=_mapping(container=options, key=str(code)), + ) + results.append(_result(code=str(code), path=None, findings=findings)) + return results + + +def _requested_file(item: object) -> tuple[SqlFile, list[str]]: + fields: Mapping[str, object] = item if isinstance(item, Mapping) else {} + file: SqlFile = sql_file_from_facts( + path=str(fields["path"]), + source=str(fields["source"]), + dialect=str(fields["dialect"]), + facts=_mapping(container=fields, key="facts"), + ) + return file, [str(code) for code in _list(container=fields, key="rules")] + + +def _definition(*, definitions: Mapping[str, RuleDefinition], code: str) -> RuleDefinition: + definition: RuleDefinition | None = definitions.get(code) + if definition is None: + raise RuleEvaluationError(f"custom Rule {code} is no longer defined") + return definition + + +def _rule_info(definition: RuleDefinition) -> _Json: + return { + "code": definition.code, + "message": definition.message, + "remediation": definition.remediation, + "enabled_by_default": definition.enabled_by_default, + "custom": True, + "subject": definition.subject.value, + "fixable": None if definition.fixable is None else definition.fixable.value, + "module": definition.module, + } + + +def _result(*, code: str, path: str | None, findings: tuple[Finding, ...]) -> _Json: + return {"code": code, "path": path, "findings": [_finding(finding) for finding in findings]} + + +def _finding(finding: Finding) -> _Json: + return { + "path": finding.path, + "start": finding.start, + "end": finding.end, + "message": finding.message, + "remediation": finding.remediation, + "edits": [_edit(edit) for edit in finding.edits], + } + + +def _edit(edit: Edit) -> _Json: + return {"start": edit.start, "end": edit.end, "text": edit.text} + + +def _list(*, container: Mapping[str, object], key: str) -> Sequence[object]: + value: object = container.get(key, ()) + return value if isinstance(value, list) else () + + +def _mapping(*, container: Mapping[str, object], key: str) -> Mapping[str, object]: + value: object = container.get(key, {}) + return value if isinstance(value, Mapping) else {} diff --git a/src/sqlclean/hosting/_helpers/sql_files.py b/src/sqlclean/hosting/_helpers/sql_files.py new file mode 100644 index 0000000..ca7b14f --- /dev/null +++ b/src/sqlclean/hosting/_helpers/sql_files.py @@ -0,0 +1,126 @@ +"""Statement facts, with character offsets, turned into located Rule subjects.""" + +from bisect import bisect_right +from collections.abc import Mapping, Sequence +from typing import TypedDict + +from sqlclean.hosting.constants import NEWLINE +from sqlclean.rules.models import ( + ColumnReference, + Cte, + FunctionCall, + Join, + Literal, + SqlFile, + Star, + Statement, + Table, +) + +type _Facts = Mapping[str, object] + + +class _Location(TypedDict): + text: str + start: int + end: int + line: int + column: int + + +class _Locator: + def __init__(self, source: str) -> None: + self.source: str = source + self._starts: list[int] = [0] + [ + index + 1 for index, character in enumerate(source) if character == NEWLINE + ] + + def located(self, facts: _Facts) -> _Location: + start: int = _integer(facts=facts, key="start") + end: int = _integer(facts=facts, key="end") + line: int = bisect_right(self._starts, start) + return _Location( + text=self.source[start:end], + start=start, + end=end, + line=line, + column=start - self._starts[line - 1] + 1, + ) + + def cte(self, item: _Facts) -> Cte: + return Cte(name=str(item["name"]), **self.located(item)) + + def table(self, item: _Facts) -> Table: + return Table( + name=str(item["name"]), + qualified_name=str(item["qualified_name"]), + alias=_optional(facts=item, key="alias"), + **self.located(item), + ) + + def join(self, item: _Facts) -> Join: + return Join( + kind=str(item["kind"]), + relation=_optional(facts=item, key="relation"), + has_condition=bool(item["has_condition"]), + **self.located(item), + ) + + def star(self, item: _Facts) -> Star: + return Star(qualifier=_optional(facts=item, key="qualifier"), **self.located(item)) + + def function(self, item: _Facts) -> FunctionCall: + return FunctionCall(name=str(item["name"]), **self.located(item)) + + def column(self, item: _Facts) -> ColumnReference: + return ColumnReference( + name=str(item["name"]), + qualifier=_optional(facts=item, key="qualifier"), + **self.located(item), + ) + + def literal(self, item: _Facts) -> Literal: + return Literal(kind=str(item["kind"]), **self.located(item)) + + +def sql_file_from_facts(*, path: str, source: str, dialect: str, facts: _Facts) -> SqlFile: + """Return the SqlFile for one file's native facts.""" + + locator: _Locator = _Locator(source) + statements: tuple[Statement, ...] = tuple( + _statement(path=path, dialect=dialect, facts=statement, locator=locator) + for statement in _items(facts=facts, key="statements") + ) + return SqlFile(path=path, source=source, dialect=dialect, statements=statements) + + +def _statement(*, path: str, dialect: str, facts: _Facts, locator: _Locator) -> Statement: + return Statement( + path=path, + index=_integer(facts=facts, key="index"), + kind=str(facts["kind"]), + dialect=dialect, + ctes=tuple(locator.cte(item) for item in _items(facts=facts, key="ctes")), + tables=tuple(locator.table(item) for item in _items(facts=facts, key="tables")), + joins=tuple(locator.join(item) for item in _items(facts=facts, key="joins")), + stars=tuple(locator.star(item) for item in _items(facts=facts, key="stars")), + functions=tuple(locator.function(item) for item in _items(facts=facts, key="functions")), + columns=tuple(locator.column(item) for item in _items(facts=facts, key="columns")), + literals=tuple(locator.literal(item) for item in _items(facts=facts, key="literals")), + **locator.located(facts), + ) + + +def _items(*, facts: _Facts, key: str) -> Sequence[_Facts]: + value: object = facts.get(key, ()) + return value if isinstance(value, list) else () + + +def _integer(*, facts: _Facts, key: str) -> int: + value: object = facts[key] + return value if isinstance(value, int) else 0 + + +def _optional(*, facts: _Facts, key: str) -> str | None: + value: object = facts.get(key) + return None if value is None else str(value) diff --git a/src/sqlclean/hosting/constants.py b/src/sqlclean/hosting/constants.py new file mode 100644 index 0000000..d4ce985 --- /dev/null +++ b/src/sqlclean/hosting/constants.py @@ -0,0 +1,9 @@ +"""Names in the JSON the sqlclean command exchanges with this host.""" + +CATALOGUE_MODE: str = "catalogue" +EVALUATE_MODE: str = "evaluate" +NEWLINE: str = "\n" +PYTHON_SUFFIX: str = ".py" +SKIPPED_MODULE_PREFIX: str = "test_" +SKIPPED_MODULE_NAMES: frozenset[str] = frozenset({"conftest.py", "__init__.py"}) +SKIPPED_DIRECTORIES: frozenset[str] = frozenset({"__pycache__"}) diff --git a/src/sqlclean/hosting/exceptions.py b/src/sqlclean/hosting/exceptions.py new file mode 100644 index 0000000..520908b --- /dev/null +++ b/src/sqlclean/hosting/exceptions.py @@ -0,0 +1,11 @@ +"""Errors the custom Rule host reports to the sqlclean command.""" + +from sqlclean.rules.exceptions import SqlCleanError + + +class RuleLoadingError(SqlCleanError): + """A custom Rule module could not be imported, or two Rules share a code.""" + + +class RuleEvaluationError(SqlCleanError): + """A custom Rule raised an exception or returned something other than findings.""" diff --git a/src/sqlclean/hosting/main/__init__.py b/src/sqlclean/hosting/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/hosting/main/build_sql_file.py b/src/sqlclean/hosting/main/build_sql_file.py new file mode 100644 index 0000000..c9613a7 --- /dev/null +++ b/src/sqlclean/hosting/main/build_sql_file.py @@ -0,0 +1,12 @@ +"""Build the SqlFile a Rule receives from the native statement facts.""" + +from collections.abc import Mapping + +from sqlclean.hosting._helpers.sql_files import sql_file_from_facts +from sqlclean.rules.models import SqlFile + + +def build_sql_file(*, path: str, source: str, dialect: str, facts: Mapping[str, object]) -> SqlFile: + """Return the file with its statements and their located structures.""" + + return sql_file_from_facts(path=path, source=source, dialect=dialect, facts=facts) diff --git a/src/sqlclean/hosting/main/evaluate_definition.py b/src/sqlclean/hosting/main/evaluate_definition.py new file mode 100644 index 0000000..418a32c --- /dev/null +++ b/src/sqlclean/hosting/main/evaluate_definition.py @@ -0,0 +1,17 @@ +"""Evaluate one custom Rule over files or a repository.""" + +from collections.abc import Mapping + +from sqlclean.hosting._helpers.evaluation import evaluate +from sqlclean.rules.models import Finding, RuleDefinition, SqlFile + + +def evaluate_definition( + *, + definition: RuleDefinition, + files: tuple[SqlFile, ...], + options: Mapping[str, object], +) -> tuple[Finding, ...]: + """Return the Rule's findings: per statement or file, or once over every file.""" + + return evaluate(definition=definition, files=files, options=options) diff --git a/src/sqlclean/hosting/main/serve.py b/src/sqlclean/hosting/main/serve.py new file mode 100644 index 0000000..34c7c52 --- /dev/null +++ b/src/sqlclean/hosting/main/serve.py @@ -0,0 +1,15 @@ +"""Answer one request from the sqlclean command on standard input and output.""" + +import json +import sys + +from sqlclean.hosting._helpers.requests import handle_request + + +def serve() -> int: + """Read one JSON request, write one JSON response and return the exit status.""" + + request: dict[str, object] = json.loads(sys.stdin.read()) + response: dict[str, object] = handle_request(request) + sys.stdout.write(json.dumps(response)) + return 0 diff --git a/src/sqlclean/py.typed b/src/sqlclean/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rule_testing/__init__.py b/src/sqlclean/rule_testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rule_testing/_helpers/__init__.py b/src/sqlclean/rule_testing/_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rule_testing/_helpers/harness.py b/src/sqlclean/rule_testing/_helpers/harness.py new file mode 100644 index 0000000..9c5be80 --- /dev/null +++ b/src/sqlclean/rule_testing/_helpers/harness.py @@ -0,0 +1,66 @@ +"""Evaluate a Rule over a case twice, in opposite file orders, and compare.""" + +from sqlclean.hosting.main.build_sql_file import build_sql_file +from sqlclean.hosting.main.evaluate_definition import evaluate_definition +from sqlclean.rule_testing._helpers.native_facts import native_facts +from sqlclean.rule_testing.exceptions import RuleCaseError +from sqlclean.rule_testing.models import RuleCase, RuleFile, RuleResult +from sqlclean.rules.constants import RULE_ATTRIBUTE +from sqlclean.rules.models import Edit, Finding, RuleDefinition, SqlFile +from sqlclean.rules.types import RuleCheck + + +def run_case(*, rule: RuleCheck, test_case: RuleCase) -> RuleResult: + """Return the Rule's result on the case after checking count and determinism.""" + + definition: object = getattr(rule, RULE_ATTRIBUTE, None) + if not isinstance(definition, RuleDefinition): + raise RuleCaseError("evaluate_rule needs a function decorated with @rule") + files: tuple[SqlFile, ...] = tuple( + build_sql_file( + path=file.path, + source=file.source, + dialect=test_case.dialect, + facts=native_facts(source=file.source, dialect=test_case.dialect), + ) + for file in (RuleFile(path=test_case.path, source=test_case.source), *test_case.files) + ) + first: tuple[Finding, ...] = evaluate_definition( + definition=definition, files=files, options=test_case.options + ) + second: tuple[Finding, ...] = evaluate_definition( + definition=definition, files=tuple(reversed(files)), options=test_case.options + ) + if sorted(first, key=_order) != sorted(second, key=_order): + raise RuleCaseError(f"custom Rule {definition.code} is not deterministic") + findings: tuple[Finding, ...] = tuple(sorted(first, key=_order)) + if len(findings) != test_case.expected_finding_count: + raise RuleCaseError( + f"{test_case.description}: expected {test_case.expected_finding_count} findings, " + f"got {len(findings)}" + ) + return RuleResult( + findings=findings, fixed_source=_fixed(test_case=test_case, findings=findings) + ) + + +def _order(finding: Finding) -> tuple[str, int, int, str]: + return finding.path, finding.start, finding.end, finding.message + + +def _fixed(*, test_case: RuleCase, findings: tuple[Finding, ...]) -> str: + edits: list[Edit] = [] + for finding in findings: + if finding.path == test_case.path: + edits.extend(finding.edits) + edits.sort(key=lambda edit: (edit.start, edit.end)) + pieces: list[str] = [] + cursor: int = 0 + for edit in edits: + if edit.start < cursor: + continue + pieces.append(test_case.source[cursor : edit.start]) + pieces.append(edit.text) + cursor = edit.end + pieces.append(test_case.source[cursor:]) + return "".join(pieces) diff --git a/src/sqlclean/rule_testing/_helpers/native_facts.py b/src/sqlclean/rule_testing/_helpers/native_facts.py new file mode 100644 index 0000000..510aedd --- /dev/null +++ b/src/sqlclean/rule_testing/_helpers/native_facts.py @@ -0,0 +1,39 @@ +"""Statement facts from the installed sqlclean executable, the same the command hosts use.""" + +import json +import shutil +import subprocess +import sysconfig +from pathlib import Path + +from sqlclean.rule_testing.constants import EXECUTABLE_NAME, FACTS_COMMAND +from sqlclean.rule_testing.exceptions import RuleCaseError + + +def native_facts(*, source: str, dialect: str) -> dict[str, object]: + """Return the native statement facts of source, or raise RuleCaseError if it fails.""" + + completed: subprocess.CompletedProcess[str] = subprocess.run( + [_executable(), FACTS_COMMAND, "--dialect", dialect], + input=source, + capture_output=True, + text=True, + encoding="utf-8", + check=False, + ) + if completed.returncode: + raise RuleCaseError(f"sqlclean could not parse the case SQL: {completed.stderr.strip()}") + facts: dict[str, object] = json.loads(completed.stdout) + return facts + + +def _executable() -> str: + scripts: Path = Path(sysconfig.get_path("scripts")) + for name in (EXECUTABLE_NAME, f"{EXECUTABLE_NAME}.exe"): + candidate: Path = scripts / name + if candidate.is_file(): + return str(candidate) + found: str | None = shutil.which(EXECUTABLE_NAME) + if found is None: + raise RuleCaseError("the sqlclean executable is not installed; install sqlclean") + return found diff --git a/src/sqlclean/rule_testing/constants.py b/src/sqlclean/rule_testing/constants.py new file mode 100644 index 0000000..f700b4c --- /dev/null +++ b/src/sqlclean/rule_testing/constants.py @@ -0,0 +1,6 @@ +"""Defaults of the custom Rule test harness.""" + +DEFAULT_CASE_PATH: str = "query.sql" +DEFAULT_CASE_DIALECT: str = "generic" +EXECUTABLE_NAME: str = "sqlclean" +FACTS_COMMAND: str = "facts" diff --git a/src/sqlclean/rule_testing/exceptions.py b/src/sqlclean/rule_testing/exceptions.py new file mode 100644 index 0000000..ef0bae5 --- /dev/null +++ b/src/sqlclean/rule_testing/exceptions.py @@ -0,0 +1,7 @@ +"""Failures the custom Rule test harness reports.""" + +from sqlclean.rules.exceptions import SqlCleanError + + +class RuleCaseError(SqlCleanError): + """A Rule case did not produce what it expects, or the Rule is not deterministic.""" diff --git a/src/sqlclean/rule_testing/main/__init__.py b/src/sqlclean/rule_testing/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rule_testing/main/evaluate_rule.py b/src/sqlclean/rule_testing/main/evaluate_rule.py new file mode 100644 index 0000000..33dce75 --- /dev/null +++ b/src/sqlclean/rule_testing/main/evaluate_rule.py @@ -0,0 +1,11 @@ +"""Run one custom Rule over a test case.""" + +from sqlclean.rule_testing._helpers.harness import run_case +from sqlclean.rule_testing.models import RuleCase, RuleResult +from sqlclean.rules.types import RuleCheck + + +def evaluate_rule(*, rule: RuleCheck, test_case: RuleCase) -> RuleResult: + """Return the Rule's findings on the case, failing when the count or determinism differs.""" + + return run_case(rule=rule, test_case=test_case) diff --git a/src/sqlclean/rule_testing/models.py b/src/sqlclean/rule_testing/models.py new file mode 100644 index 0000000..9ee5aaf --- /dev/null +++ b/src/sqlclean/rule_testing/models.py @@ -0,0 +1,42 @@ +"""One custom Rule test case and its result.""" + +from collections.abc import Mapping +from dataclasses import dataclass, field + +from sqlclean.rule_testing.constants import DEFAULT_CASE_DIALECT, DEFAULT_CASE_PATH +from sqlclean.rules.models import Finding + + +@dataclass(frozen=True, slots=True) +class RuleFile: + """A supporting SQL file for Rules that read more than the case's own file.""" + + path: str + source: str + + +@dataclass(frozen=True, slots=True) +class RuleCase: + """SQL to check and the number of findings the Rule must report on it.""" + + description: str + source: str + expected_finding_count: int + path: str = DEFAULT_CASE_PATH + dialect: str = DEFAULT_CASE_DIALECT + files: tuple[RuleFile, ...] = () + options: Mapping[str, object] = field(default_factory=dict) + + +@dataclass(frozen=True, slots=True) +class RuleResult: + """The Rule's findings, and the case source with every fix applied.""" + + findings: tuple[Finding, ...] + fixed_source: str + + @property + def finding_count(self) -> int: + """Return how many findings the Rule reported.""" + + return len(self.findings) diff --git a/src/sqlclean/rules/__init__.py b/src/sqlclean/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rules/_helpers/__init__.py b/src/sqlclean/rules/_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rules/_helpers/definition.py b/src/sqlclean/rules/_helpers/definition.py new file mode 100644 index 0000000..22774d2 --- /dev/null +++ b/src/sqlclean/rules/_helpers/definition.py @@ -0,0 +1,107 @@ +"""Validate a @rule declaration and attach its definition to the function.""" + +import inspect +from collections.abc import Callable +from typing import get_type_hints + +from sqlclean.rules.classes.rule_context import RuleContext +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.constants import ( + CUSTOM_CODE_PREFIX, + RULE_ATTRIBUTE, + RULE_CODE_DIGIT_COUNT, + RULE_PARAMETER_COUNT, + SIGNATURE_REQUIREMENT, +) +from sqlclean.rules.exceptions import RuleDefinitionError +from sqlclean.rules.models import Repository, RuleDefinition, SqlFile, Statement +from sqlclean.rules.types import FixSafety, RuleCheck, RuleSubjectKind + +_SUBJECTS: dict[type, RuleSubjectKind] = { + Statement: RuleSubjectKind.STATEMENT, + SqlFile: RuleSubjectKind.FILE, + Repository: RuleSubjectKind.REPOSITORY, +} + + +def define_rule( + *, + code: str, + slug: str, + message: str, + remediation: str, + options: tuple[RuleOption[object], ...], + enabled_by_default: bool, + fixable: FixSafety | None, +) -> Callable[[RuleCheck], RuleCheck]: + """Return the decorator that validates and attaches one Rule definition.""" + + if not all(value.strip() for value in (code, slug, message, remediation)): + raise RuleDefinitionError(f"custom Rule {code} needs a code, slug, message and remediation") + if not is_custom_code(code): + raise RuleDefinitionError( + f"custom Rule code must be {CUSTOM_CODE_PREFIX}, optional capital letters and " + f"{RULE_CODE_DIGIT_COUNT} digits, such as XSQCNAME001: {code}" + ) + names: list[str] = [option.name for option in options] + if len(names) != len(set(names)): + raise RuleDefinitionError(f"custom Rule {code} declares an option name twice") + + def decorate(check: RuleCheck) -> RuleCheck: + subject, subject_parameter, context_parameter = _signature(code=code, check=check) + definition: RuleDefinition = RuleDefinition( + code=code, + slug=slug, + message=message, + remediation=remediation, + check=check, + subject=subject, + subject_parameter=subject_parameter, + context_parameter=context_parameter, + options=options, + enabled_by_default=enabled_by_default, + fixable=fixable, + module=check.__module__, + ) + setattr(check, RULE_ATTRIBUTE, definition) + return check + + return decorate + + +def is_custom_code(code: str) -> bool: + """Return whether code is XSQC, optional capital letters and three digits.""" + + family: str = code[:-RULE_CODE_DIGIT_COUNT] + number: str = code[-RULE_CODE_DIGIT_COUNT:] + letters: str = family.removeprefix(CUSTOM_CODE_PREFIX) + return ( + family.startswith(CUSTOM_CODE_PREFIX) + and (not letters or (letters.isascii() and letters.isalpha() and letters.isupper())) + and number.isascii() + and number.isdecimal() + and len(number) == RULE_CODE_DIGIT_COUNT + ) + + +def _signature(*, code: str, check: RuleCheck) -> tuple[RuleSubjectKind, str, str]: + parameters: tuple[inspect.Parameter, ...] = tuple(inspect.signature(check).parameters.values()) + if len(parameters) != RULE_PARAMETER_COUNT or any( + parameter.kind is not inspect.Parameter.KEYWORD_ONLY for parameter in parameters + ): + raise RuleDefinitionError(f"custom Rule {code} {SIGNATURE_REQUIREMENT}") + try: + hints: dict[str, object] = get_type_hints(check) + except (NameError, TypeError) as error: + raise RuleDefinitionError( + f"could not resolve custom Rule {code} annotations: {error}" + ) from error + subjects: list[tuple[RuleSubjectKind, str]] = [ + (_SUBJECTS[hint], name) + for name, hint in hints.items() + if isinstance(hint, type) and hint in _SUBJECTS + ] + contexts: list[str] = [name for name, hint in hints.items() if hint is RuleContext] + if len(subjects) != 1 or len(contexts) != 1: + raise RuleDefinitionError(f"custom Rule {code} {SIGNATURE_REQUIREMENT}") + return subjects[0][0], subjects[0][1], contexts[0] diff --git a/src/sqlclean/rules/classes/__init__.py b/src/sqlclean/rules/classes/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rules/classes/rule_context.py b/src/sqlclean/rules/classes/rule_context.py new file mode 100644 index 0000000..eab7a28 --- /dev/null +++ b/src/sqlclean/rules/classes/rule_context.py @@ -0,0 +1,99 @@ +"""What a Rule receives besides its subject: options, findings and fix edits.""" + +from collections.abc import Mapping, Sequence +from typing import cast + +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.exceptions import RuleUsageError +from sqlclean.rules.models import Edit, Finding, RuleDefinition, SqlFile, Statement +from sqlclean.rules.types import Located, RuleOptionValue + + +class RuleContext: + """The per-evaluation API of one Rule: typed options, findings and edits.""" + + def __init__( + self, + *, + definition: RuleDefinition, + options: Mapping[str, RuleOptionValue], + file: SqlFile | None, + ) -> None: + self._definition: RuleDefinition = definition + self._options: Mapping[str, RuleOptionValue] = options + self._file: SqlFile | None = file + + @property + def file(self) -> SqlFile | None: + """Return the file being checked, or None for a Repository subject.""" + + return self._file + + @property + def dialect(self) -> str | None: + """Return the dialect of the file being checked, or None for a Repository subject.""" + + return None if self._file is None else self._file.dialect + + def option[T](self, option: RuleOption[T]) -> T: + """Return the configured value of one declared option, or its default.""" + + if all(declared is not option for declared in self._definition.options): + raise RuleUsageError( + f"Rule {self._definition.code} reads undeclared option {option.name}" + ) + if option.name not in self._options: + return option.default + return cast(T, self._options[option.name]) + + def finding( + self, + *, + subject: Statement | SqlFile, + node: Located | None = None, + message: str | None = None, + remediation: str | None = None, + fix: Sequence[Edit] = (), + ) -> Finding: + """Return a finding at node, or at the subject, with an optional fix.""" + + if fix and self._definition.fixable is None: + raise RuleUsageError( + f"Rule {self._definition.code} returned a fix but does not declare fixable" + ) + located: Located = node if node is not None else _subject_location(subject) + return Finding( + code=self._definition.code, + path=subject.path, + start=located.start, + end=located.end, + message=message or self._definition.message, + remediation=remediation or self._definition.remediation, + edits=tuple(fix), + ) + + def replace(self, *, node: Located, text: str) -> Edit: + """Return an edit that replaces node's text.""" + + return Edit(start=node.start, end=node.end, text=text) + + def insert_before(self, *, node: Located, text: str) -> Edit: + """Return an edit that inserts text immediately before node.""" + + return Edit(start=node.start, end=node.start, text=text) + + def insert_after(self, *, node: Located, text: str) -> Edit: + """Return an edit that inserts text immediately after node.""" + + return Edit(start=node.end, end=node.end, text=text) + + def delete(self, *, node: Located) -> Edit: + """Return an edit that removes node's text.""" + + return Edit(start=node.start, end=node.end, text="") + + +def _subject_location(subject: Statement | SqlFile) -> Located: + if isinstance(subject, SqlFile): + return Edit(start=0, end=0, text="") + return subject diff --git a/src/sqlclean/rules/classes/rule_option.py b/src/sqlclean/rules/classes/rule_option.py new file mode 100644 index 0000000..a259fde --- /dev/null +++ b/src/sqlclean/rules/classes/rule_option.py @@ -0,0 +1,128 @@ +"""One typed configuration option a Rule declares.""" + +from typing import cast + +from sqlclean.rules.exceptions import RuleDefinitionError, RuleOptionError + + +class RuleOption[T]: + """A named, typed option configured under [rules.rule_options.].""" + + def __init__( + self, + *, + name: str, + value_type: type, + default: T, + description: str, + choices: tuple[T, ...] = (), + minimum: int | None = None, + maximum: int | None = None, + item_type: type | None = None, + ) -> None: + if not name.isidentifier(): + raise RuleDefinitionError(f"Rule option name must be an identifier: {name!r}") + self.name: str = name + self.value_type: type = value_type + self.default: T = default + self.description: str = description + self.choices: tuple[T, ...] = choices + self.minimum: int | None = minimum + self.maximum: int | None = maximum + self.item_type: type | None = item_type + _ = self.parse(default) + + @classmethod + def boolean(cls, *, name: str, default: bool, description: str) -> "RuleOption[bool]": + """Declare a true or false option.""" + + return RuleOption[bool]( + name=name, value_type=bool, default=default, description=description + ) + + @classmethod + def integer( + cls, + *, + name: str, + default: int, + description: str, + minimum: int | None = None, + maximum: int | None = None, + ) -> "RuleOption[int]": + """Declare a whole-number option with optional bounds.""" + + return RuleOption[int]( + name=name, + value_type=int, + default=default, + description=description, + minimum=minimum, + maximum=maximum, + ) + + @classmethod + def string( + cls, *, name: str, default: str, description: str, choices: tuple[str, ...] = () + ) -> "RuleOption[str]": + """Declare a text option, optionally limited to choices.""" + + return RuleOption[str]( + name=name, value_type=str, default=default, description=description, choices=choices + ) + + @classmethod + def string_list( + cls, *, name: str, default: tuple[str, ...], description: str + ) -> "RuleOption[tuple[str, ...]]": + """Declare a list-of-text option.""" + + return RuleOption[tuple[str, ...]]( + name=name, value_type=tuple, default=default, description=description, item_type=str + ) + + @classmethod + def integer_list( + cls, *, name: str, default: tuple[int, ...], description: str + ) -> "RuleOption[tuple[int, ...]]": + """Declare a list-of-whole-numbers option.""" + + return RuleOption[tuple[int, ...]]( + name=name, value_type=tuple, default=default, description=description, item_type=int + ) + + def parse(self, value: object) -> T: + """Return a configured value as this option's type, or raise RuleOptionError.""" + + if self.item_type is not None: + return self._parse_list(value) + if not self._is_scalar(value=value, expected=self.value_type): + raise RuleOptionError( + f"option {self.name} expects {self.value_type.__name__}, got {value!r}" + ) + if self.choices and value not in self.choices: + raise RuleOptionError(f"option {self.name} must be one of {list(self.choices)!r}") + if isinstance(value, int) and not isinstance(value, bool): + self._check_bounds(value) + return cast(T, value) + + def _parse_list(self, value: object) -> T: + if not isinstance(value, list | tuple): + raise RuleOptionError(f"option {self.name} expects a list, got {value!r}") + items: tuple[object, ...] = tuple(value) + item_type: type = self.item_type or str + if not all(self._is_scalar(value=item, expected=item_type) for item in items): + raise RuleOptionError(f"option {self.name} expects a list of {item_type.__name__}") + return cast(T, items) + + def _check_bounds(self, value: int) -> None: + if self.minimum is not None and value < self.minimum: + raise RuleOptionError(f"option {self.name} must be at least {self.minimum}") + if self.maximum is not None and value > self.maximum: + raise RuleOptionError(f"option {self.name} must be at most {self.maximum}") + + @staticmethod + def _is_scalar(*, value: object, expected: type) -> bool: + if expected is int and isinstance(value, bool): + return False + return isinstance(value, expected) diff --git a/src/sqlclean/rules/constants.py b/src/sqlclean/rules/constants.py new file mode 100644 index 0000000..878101b --- /dev/null +++ b/src/sqlclean/rules/constants.py @@ -0,0 +1,12 @@ +"""Constants of the custom Rule authoring API.""" + +RULE_ATTRIBUTE: str = "__sqlclean_rule__" +CUSTOM_CODE_PREFIX: str = "XSQC" +RULE_CODE_DIGIT_COUNT: int = 3 +RULE_PARAMETER_COUNT: int = 2 +SIGNATURE_REQUIREMENT: str = ( + "must accept exactly one typed Statement, SqlFile or Repository subject and one typed " + "RuleContext, both keyword-only" +) +POLYGLOT_MODULE: str = "polyglot_sql" +POLYGLOT_INSTALL_HINT: str = "install the ast extra: pip install 'sqlclean[ast]'" diff --git a/src/sqlclean/rules/exceptions.py b/src/sqlclean/rules/exceptions.py new file mode 100644 index 0000000..ff3d556 --- /dev/null +++ b/src/sqlclean/rules/exceptions.py @@ -0,0 +1,17 @@ +"""Errors of the custom Rule authoring API.""" + + +class SqlCleanError(Exception): + """Base class of every error SQLClean raises.""" + + +class RuleDefinitionError(SqlCleanError): + """A @rule declaration is invalid.""" + + +class RuleOptionError(SqlCleanError): + """A configured Rule option value is invalid.""" + + +class RuleUsageError(SqlCleanError): + """A Rule used the RuleContext API incorrectly.""" diff --git a/src/sqlclean/rules/main/__init__.py b/src/sqlclean/rules/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/sqlclean/rules/main/define.py b/src/sqlclean/rules/main/define.py new file mode 100644 index 0000000..f3a5436 --- /dev/null +++ b/src/sqlclean/rules/main/define.py @@ -0,0 +1,30 @@ +"""The @rule decorator that declares a custom Rule.""" + +from collections.abc import Callable + +from sqlclean.rules._helpers.definition import define_rule +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.types import FixSafety, RuleCheck + + +def rule( + *, + code: str, + message: str, + remediation: str, + slug: str | None = None, + options: tuple[RuleOption[object], ...] = (), + enabled_by_default: bool = False, + fixable: FixSafety | str | None = None, +) -> Callable[[RuleCheck], RuleCheck]: + """Declare one custom Rule while returning the function unchanged.""" + + return define_rule( + code=code, + slug=slug if slug is not None else code.lower(), + message=message, + remediation=remediation, + options=options, + enabled_by_default=enabled_by_default, + fixable=None if fixable is None else FixSafety(fixable), + ) diff --git a/src/sqlclean/rules/models.py b/src/sqlclean/rules/models.py new file mode 100644 index 0000000..ab8c093 --- /dev/null +++ b/src/sqlclean/rules/models.py @@ -0,0 +1,194 @@ +"""Structured values Rules receive and return.""" + +from dataclasses import dataclass, field +from functools import cached_property +from importlib import import_module +from types import ModuleType + +from sqlclean.rules.classes.rule_option import RuleOption +from sqlclean.rules.constants import POLYGLOT_INSTALL_HINT, POLYGLOT_MODULE +from sqlclean.rules.exceptions import RuleUsageError +from sqlclean.rules.types import FixSafety, RuleCheck, RuleSubjectKind + + +@dataclass(frozen=True, slots=True) +class Edit: + """Replace the characters start..end of a file with text.""" + + start: int + end: int + text: str + + +@dataclass(frozen=True, slots=True) +class Finding: + """One Rule violation located by character offsets in its file.""" + + code: str + path: str + start: int + end: int + message: str + remediation: str + edits: tuple[Edit, ...] = () + + +@dataclass(frozen=True, slots=True) +class Cte: + """A common table expression, located at its name.""" + + name: str + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class Table: + """A relation read in FROM or JOIN, located at its name.""" + + name: str + qualified_name: str + alias: str | None + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class Join: + """One join, located at the relation it joins.""" + + kind: str + relation: str | None + has_condition: bool + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class Star: + """A * or qualifier.* projection.""" + + qualifier: str | None + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class FunctionCall: + """A call of a named function, located at its name.""" + + name: str + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class ColumnReference: + """A column reference with its qualifier when written.""" + + name: str + qualifier: str | None + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True, slots=True) +class Literal: + """A string or numeric literal as written.""" + + kind: str + text: str + start: int + end: int + line: int + column: int + + +@dataclass(frozen=True) +class Statement: + """One statement of a SQL file, located by character offsets in that file.""" + + path: str + index: int + kind: str + text: str + start: int + end: int + line: int + column: int + dialect: str + ctes: tuple[Cte, ...] + tables: tuple[Table, ...] + joins: tuple[Join, ...] + stars: tuple[Star, ...] + functions: tuple[FunctionCall, ...] + columns: tuple[ColumnReference, ...] + literals: tuple[Literal, ...] + + def polyglot_ast(self) -> object: + """Return Polyglot's syntax tree of this statement, parsed on first use.""" + + return self._polyglot_ast + + @cached_property + def _polyglot_ast(self) -> object: + try: + polyglot: ModuleType = import_module(POLYGLOT_MODULE) + except ImportError as error: + raise RuleUsageError( + f"polyglot_ast() needs the Polyglot SQL package: {POLYGLOT_INSTALL_HINT}" + ) from error + return polyglot.parse_one(self.text, dialect=self.dialect) + + +@dataclass(frozen=True) +class SqlFile: + """One SQL file and its statements.""" + + path: str + source: str + dialect: str + statements: tuple[Statement, ...] + + +@dataclass(frozen=True) +class Repository: + """Every checked SQL file, for Rules about conventions across files.""" + + files: tuple[SqlFile, ...] + + +@dataclass(frozen=True, slots=True) +class RuleDefinition: + """The metadata @rule attaches to a Rule function.""" + + code: str + slug: str + message: str + remediation: str + check: RuleCheck + subject: RuleSubjectKind + subject_parameter: str + context_parameter: str + options: tuple[RuleOption[object], ...] = () + enabled_by_default: bool = False + fixable: FixSafety | None = None + module: str = field(default="") diff --git a/src/sqlclean/rules/types.py b/src/sqlclean/rules/types.py new file mode 100644 index 0000000..82d25fb --- /dev/null +++ b/src/sqlclean/rules/types.py @@ -0,0 +1,33 @@ +"""Type declarations of the custom Rule authoring API.""" + +from collections.abc import Callable, Sequence +from enum import StrEnum +from typing import Protocol + +type RuleOptionValue = bool | int | str | tuple[str, ...] | tuple[int, ...] +type RuleCheck = Callable[..., Sequence[object]] + + +class FixSafety(StrEnum): + """Whether a Rule's fix keeps query results (safe) or may change them (unsafe).""" + + SAFE = "safe" + UNSAFE = "unsafe" + + +class RuleSubjectKind(StrEnum): + """The subject a Rule evaluates.""" + + STATEMENT = "statement" + FILE = "file" + REPOSITORY = "repository" + + +class Located(Protocol): + """Anything with a character range in its file, such as a statement or a SQL node.""" + + @property + def start(self) -> int: ... + + @property + def end(self) -> int: ... diff --git a/src/sqlclean/testing.py b/src/sqlclean/testing.py new file mode 100644 index 0000000..2be0bbc --- /dev/null +++ b/src/sqlclean/testing.py @@ -0,0 +1,6 @@ +"""Test harness for custom Rules.""" + +from sqlclean.rule_testing.main.evaluate_rule import evaluate_rule +from sqlclean.rule_testing.models import RuleCase, RuleFile, RuleResult + +__all__ = ["RuleCase", "RuleFile", "RuleResult", "evaluate_rule"] diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/__init__.py b/tests/e2e/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/src/__init__.py b/tests/e2e/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/src/sqlclean/__init__.py b/tests/e2e/src/sqlclean/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/src/sqlclean/hosting/__init__.py b/tests/e2e/src/sqlclean/hosting/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py new file mode 100644 index 0000000..b6dbbb6 --- /dev/null +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -0,0 +1,51 @@ +"""Test case types for the sqlclean command.""" + +from __future__ import annotations + +from dataclasses import dataclass, field + + +@dataclass(frozen=True) +class CheckTestCase: + """A project, a check invocation and the findings and exit status it must give.""" + + description: str + config: str + files: dict[str, str] + arguments: tuple[str, ...] + expected_exit: int + expected_codes: tuple[str, ...] + expected_stale: tuple[str, ...] = () + + +@dataclass(frozen=True) +class RewriteTestCase: + """A project, a fix or format invocation and the file contents it must leave.""" + + description: str + config: str + files: dict[str, str] + arguments: tuple[str, ...] + expected_exit: int + expected_files: dict[str, str] = field(default_factory=dict) + + +@dataclass(frozen=True) +class IdempotencyTestCase: + """Unformatted SQL that a second format run must leave unchanged.""" + + description: str + source: str + expected_exits: tuple[int, int] + + +@dataclass(frozen=True) +class CacheTestCase: + """A project checked, rechecked without Python, then rechecked after an edit.""" + + description: str + files: dict[str, str] + edited: dict[str, str] + expected_exits: tuple[int, int, int] + expected_cached_code: str + expected_error: str diff --git a/tests/e2e/src/sqlclean/hosting/helpers.py b/tests/e2e/src/sqlclean/hosting/helpers.py new file mode 100644 index 0000000..4d1a636 --- /dev/null +++ b/tests/e2e/src/sqlclean/hosting/helpers.py @@ -0,0 +1,45 @@ +"""Run the installed sqlclean executable on a temporary project.""" + +from __future__ import annotations + +import os +import shutil +import subprocess +import sysconfig +from pathlib import Path + + +def executable() -> str: + """Return the sqlclean executable installed beside this interpreter, else the one on PATH.""" + + found: str | None = shutil.which( + "sqlclean", path=sysconfig.get_path("scripts") + ) or shutil.which("sqlclean") + assert found is not None, "install sqlclean before running the command tests" + return found + + +def write_project(root: Path, *, config: str, files: dict[str, str]) -> None: + """Write sqlclean.toml and every file under root.""" + + (root / "sqlclean.toml").write_text(config, encoding="utf-8") + for path, text in files.items(): + target: Path = root / path + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(text, encoding="utf-8") + + +def run( + root: Path, *arguments: str, environment: dict[str, str] +) -> subprocess.CompletedProcess[str]: + """Run sqlclean with arguments in root, with environment added, and return the process.""" + + return subprocess.run( + [executable(), *arguments], + cwd=root, + capture_output=True, + text=True, + encoding="utf-8", + check=False, + env={**os.environ, **environment}, + ) diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py new file mode 100644 index 0000000..891a654 --- /dev/null +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -0,0 +1,239 @@ +"""End-to-end tests for the sqlclean command on temporary projects.""" + +from __future__ import annotations + +import json +import subprocess +from pathlib import Path + +import pytest + +from tests.e2e.src.sqlclean.hosting._test_types import ( + CacheTestCase, + CheckTestCase, + IdempotencyTestCase, + RewriteTestCase, +) +from tests.e2e.src.sqlclean.hosting.helpers import run, write_project +from tests.integration.src.sqlclean.rule_testing.helpers import SAMPLE_RULES + +DUCKDB: str = 'dialect = "duckdb"\n' +CROSS_JOIN_ONLY: str = DUCKDB + '[rules]\nselect = ["SQC038"]\n' +CUSTOM: str = DUCKDB + '[rules]\nselect = ["XSQCNAME", "XSQCSTYLE"]\ncustom = ["sql_rules"]\n' +CROSS_JOIN_EXCEPTIONS: str = ( + CROSS_JOIN_ONLY + + '[[rules.rule_exceptions]]\nrule = "SQC038"\npath = "models/q.sql"\nrelation = "b"\n' + + 'reason = "Every pair is needed."\n' + + '[[rules.rule_exceptions]]\nrule = "SQC038"\npath = "models/q.sql"\nrelation = "z"\n' + + 'reason = "Removed join."\n' +) +NULL_AND_COMMA: str = "select a.id from a, b where a.x = null\n" + + +@pytest.mark.parametrize( + "test_case", + [ + CheckTestCase( + description="a clean file passes", + config=CROSS_JOIN_ONLY, + files={"models/q.sql": "SELECT a.id FROM a\n"}, + arguments=(), + expected_exit=0, + expected_codes=(), + ), + CheckTestCase( + description="selected Rules report in file order", + config=DUCKDB, + files={"models/q.sql": NULL_AND_COMMA}, + arguments=("--select", "SQC001,SQC002"), + expected_exit=1, + expected_codes=("SQC002", "SQC001"), + ), + CheckTestCase( + description="an ignored Rule is skipped", + config=DUCKDB, + files={"models/q.sql": NULL_AND_COMMA}, + arguments=("--select", "SQC001,SQC002", "--ignore", "SQC002"), + expected_exit=1, + expected_codes=("SQC001",), + ), + CheckTestCase( + description="an exception hides its finding and an unmatched one is stale", + config=CROSS_JOIN_EXCEPTIONS, + files={"models/q.sql": "SELECT a.id FROM a CROSS JOIN b\n"}, + arguments=(), + expected_exit=1, + expected_codes=(), + expected_stale=("z",), + ), + CheckTestCase( + description="unparseable SQL is reported as a finding", + config=CROSS_JOIN_ONLY, + files={"models/q.sql": "SELECT FROM WHERE (\n"}, + arguments=(), + expected_exit=1, + expected_codes=("SQC999",), + ), + CheckTestCase( + description="custom Rules run with their configured options", + config=CUSTOM + '[rules.rule_options.XSQCNAME001]\nprefixes = ["stg_"]\n', + files={ + "sql_rules/naming.py": SAMPLE_RULES, + "models/q.sql": "SELECT COUNT(1) AS n FROM stg_orders JOIN tmp_x ON TRUE\n", + }, + arguments=(), + expected_exit=1, + expected_codes=("XSQCSTYLE001", "XSQCNAME001"), + ), + ], + ids=lambda case: case.description, +) +def test_given_project_when_checking_then_reports_findings_and_exit_status( + tmp_path: Path, test_case: CheckTestCase +) -> None: + write_project(tmp_path, config=test_case.config, files=test_case.files) + + completed: subprocess.CompletedProcess[str] = run( + tmp_path, "check", "--output-format", "json", *test_case.arguments, environment={} + ) + report: dict[str, list[dict[str, object]]] = json.loads(completed.stdout) + + assert completed.returncode == test_case.expected_exit, completed.stderr + assert tuple(finding["code"] for finding in report["findings"]) == test_case.expected_codes + assert ( + tuple(stale["relation"] for stale in report["stale_exceptions"]) == test_case.expected_stale + ) + + +@pytest.mark.parametrize( + "test_case", + [ + RewriteTestCase( + description="fix applies safe fixes, leaves unsafe ones and formats", + config=DUCKDB + '[rules]\nselect = ["SQC001", "SQC002"]\n', + files={"models/q.sql": NULL_AND_COMMA}, + arguments=("fix",), + expected_exit=1, + expected_files={ + "models/q.sql": "SELECT a.id\nFROM a\nCROSS JOIN b\nWHERE a.x = NULL\n" + }, + ), + RewriteTestCase( + description="fix with unsafe fixes applies them too and then passes", + config=DUCKDB + '[rules]\nselect = ["SQC001", "SQC002"]\n', + files={"models/q.sql": NULL_AND_COMMA}, + arguments=("fix", "--unsafe-fixes"), + expected_exit=0, + expected_files={ + "models/q.sql": "SELECT a.id\nFROM a\nCROSS JOIN b\nWHERE a.x IS NULL\n" + }, + ), + RewriteTestCase( + description="fix applies custom Rule fixes", + config=CUSTOM, + files={ + "sql_rules/naming.py": SAMPLE_RULES, + "models/q.sql": "select count(1) as n from orders\n", + }, + arguments=("fix",), + expected_exit=0, + expected_files={"models/q.sql": "SELECT COUNT(*) AS n\nFROM orders\n"}, + ), + RewriteTestCase( + description="fix --diff writes nothing", + config=DUCKDB + '[rules]\nselect = ["SQC002"]\n', + files={"models/q.sql": "select a.id from a, b\n"}, + arguments=("fix", "--diff"), + expected_exit=1, + expected_files={"models/q.sql": "select a.id from a, b\n"}, + ), + RewriteTestCase( + description="format --check fails without writing", + config=DUCKDB, + files={"models/q.sql": "select 1;select 2\n"}, + arguments=("format", "--check"), + expected_exit=1, + expected_files={"models/q.sql": "select 1;select 2\n"}, + ), + RewriteTestCase( + description="format writes each statement apart with one final newline", + config=DUCKDB, + files={"models/q.sql": "select 1;select 2"}, + arguments=("format",), + expected_exit=0, + expected_files={"models/q.sql": "SELECT 1;\n\nSELECT 2\n"}, + ), + ], + ids=lambda case: case.description, +) +def test_given_project_when_rewriting_then_files_and_exit_status_match( + tmp_path: Path, test_case: RewriteTestCase +) -> None: + write_project(tmp_path, config=test_case.config, files=test_case.files) + + completed: subprocess.CompletedProcess[str] = run( + tmp_path, *test_case.arguments, environment={} + ) + files: dict[str, str] = { + path: (tmp_path / path).read_text(encoding="utf-8") for path in test_case.expected_files + } + + assert completed.returncode == test_case.expected_exit, completed.stdout + completed.stderr + assert files == test_case.expected_files + + +@pytest.mark.parametrize( + "test_case", + [ + IdempotencyTestCase( + description="a formatted query is a fixed point of the formatter", + source="select a, b from t where a > 1 and b in (select b from u) order by a", + expected_exits=(0, 0), + ), + ], + ids=lambda case: case.description, +) +def test_given_formatted_file_when_formatting_again_then_nothing_changes( + tmp_path: Path, test_case: IdempotencyTestCase +) -> None: + write_project(tmp_path, config=DUCKDB, files={"q.sql": test_case.source}) + + first: subprocess.CompletedProcess[str] = run(tmp_path, "format", environment={}) + formatted: str = (tmp_path / "q.sql").read_text(encoding="utf-8") + second: subprocess.CompletedProcess[str] = run(tmp_path, "format", "--check", environment={}) + + assert (first.returncode, second.returncode) == test_case.expected_exits, second.stdout + assert (tmp_path / "q.sql").read_text(encoding="utf-8") == formatted + + +@pytest.mark.parametrize( + "test_case", + [ + CacheTestCase( + description="unchanged files reuse custom Rule results and edited files rerun them", + files={ + "sql_rules/naming.py": SAMPLE_RULES, + "models/q.sql": "SELECT * FROM tmp_orders\n", + }, + edited={"models/q.sql": "SELECT * FROM tmp_customers\n"}, + expected_exits=(1, 1, 2), + expected_cached_code="XSQCNAME001", + expected_error="missing-python", + ), + ], + ids=lambda case: case.description, +) +def test_given_unchanged_files_when_checking_again_then_custom_rules_come_from_the_cache( + tmp_path: Path, test_case: CacheTestCase +) -> None: + write_project(tmp_path, config=CUSTOM, files=test_case.files) + no_python: dict[str, str] = {"SQLCLEAN_PYTHON": str(tmp_path / "missing-python")} + + first: subprocess.CompletedProcess[str] = run(tmp_path, "check", environment={}) + cached: subprocess.CompletedProcess[str] = run(tmp_path, "check", environment=no_python) + write_project(tmp_path, config=CUSTOM, files=test_case.edited) + changed: subprocess.CompletedProcess[str] = run(tmp_path, "check", environment=no_python) + + assert (first.returncode, cached.returncode, changed.returncode) == test_case.expected_exits + assert test_case.expected_cached_code in cached.stdout + assert test_case.expected_error in changed.stderr diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/src/__init__.py b/tests/integration/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/src/sqlclean/__init__.py b/tests/integration/src/sqlclean/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/src/sqlclean/rule_testing/__init__.py b/tests/integration/src/sqlclean/rule_testing/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/integration/src/sqlclean/rule_testing/_test_types.py b/tests/integration/src/sqlclean/rule_testing/_test_types.py new file mode 100644 index 0000000..bb99f7e --- /dev/null +++ b/tests/integration/src/sqlclean/rule_testing/_test_types.py @@ -0,0 +1,28 @@ +"""Test case types for the custom Rule harness.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from sqlclean.rule_testing.models import RuleCase + + +@dataclass(frozen=True) +class HarnessTestCase: + """A Rule, its case and the findings and fixed source the harness must return.""" + + description: str + rule_name: str + rule_case: RuleCase + expected_texts: tuple[str, ...] + expected_fixed_source: str + + +@dataclass(frozen=True) +class HarnessFailureTestCase: + """A Rule case the harness must reject, and the reason it gives.""" + + description: str + rule_name: str + rule_case: RuleCase + expected_error: str diff --git a/tests/integration/src/sqlclean/rule_testing/helpers.py b/tests/integration/src/sqlclean/rule_testing/helpers.py new file mode 100644 index 0000000..7a479cd --- /dev/null +++ b/tests/integration/src/sqlclean/rule_testing/helpers.py @@ -0,0 +1,105 @@ +"""The sample custom Rules, as users would write them, and their loader.""" + +from __future__ import annotations + +import importlib.util +from importlib.machinery import ModuleSpec +from pathlib import Path +from types import ModuleType + +from sqlclean.rules.types import RuleCheck + +SAMPLE_RULES: str = """ +from sqlclean import FixSafety, Repository, RuleContext, RuleOption, SqlFile, Statement, rule + +MAX_JOINS = RuleOption.integer( + name="max_joins", default=1, description="Most joins one statement may use.", minimum=0 +) +PREFIXES = RuleOption.string_list( + name="prefixes", default=("tmp_",), description="Forbidden table prefixes." +) + + +@rule( + code="XSQCSHAPE001", + message="Statement joins too many relations", + remediation="Split the statement into CTEs.", + options=(MAX_JOINS,), +) +def too_many_joins(*, statement: Statement, ctx: RuleContext) -> list: + over_limit = statement.joins[ctx.option(MAX_JOINS):] + return [ctx.finding(subject=statement, node=join) for join in over_limit[:1]] + + +@rule( + code="XSQCNAME001", + message="Table name uses a forbidden prefix", + remediation="Read from the published table.", + options=(PREFIXES,), + enabled_by_default=True, +) +def forbidden_prefix(*, statement: Statement, ctx: RuleContext) -> list: + prefixes = ctx.option(PREFIXES) + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith(prefixes) + ] + + +@rule( + code="XSQCSTYLE001", + message="COUNT(1) should be COUNT(*)", + remediation="Write COUNT(*).", + fixable=FixSafety.SAFE, + enabled_by_default=True, +) +def count_one(*, statement: Statement, ctx: RuleContext) -> list: + counts = [function for function in statement.functions if function.name.upper() == "COUNT"] + called = {function.end + 1 for function in counts} + return [ + ctx.finding(subject=statement, node=literal, fix=[ctx.replace(node=literal, text="*")]) + for literal in statement.literals + if literal.text == "1" and literal.start in called + ] + + +@rule( + code="XSQCFILE001", + message="File holds more than one statement", + remediation="Keep one statement per file.", +) +def one_statement(*, file: SqlFile, ctx: RuleContext) -> list: + return [ctx.finding(subject=file)] if len(file.statements) > 1 else [] + + +@rule( + code="XSQCREPO001", + message="Table is read by only one file", + remediation="Inline the table or share it.", +) +def single_reader(*, repository: Repository, ctx: RuleContext) -> list: + readers = {} + for file in repository.files: + for statement in file.statements: + for table in statement.tables: + readers.setdefault(table.name, []).append((statement, table)) + return [ + ctx.finding(subject=uses[0][0], node=uses[0][1]) + for uses in readers.values() + if len({statement.path for statement, _ in uses}) == 1 + ] +""" + + +def sample_rule(directory: Path, name: str) -> RuleCheck: + """Return the sample Rule function called name, imported from a module under directory.""" + + path: Path = directory / "sample_rules.py" + path.write_text(SAMPLE_RULES, encoding="utf-8") + spec: ModuleSpec | None = importlib.util.spec_from_file_location("sample_rules", path) + assert spec is not None and spec.loader is not None + module: ModuleType = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + check: RuleCheck = getattr(module, name) + return check diff --git a/tests/integration/src/sqlclean/rule_testing/test_evaluate_rule.py b/tests/integration/src/sqlclean/rule_testing/test_evaluate_rule.py new file mode 100644 index 0000000..d04a50c --- /dev/null +++ b/tests/integration/src/sqlclean/rule_testing/test_evaluate_rule.py @@ -0,0 +1,149 @@ +"""Tests for evaluate_rule over SQL parsed by the native sqlclean executable.""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from sqlclean.rules.exceptions import SqlCleanError +from sqlclean.rules.types import RuleCheck +from sqlclean.testing import RuleCase, RuleFile, RuleResult, evaluate_rule +from tests.integration.src.sqlclean.rule_testing._test_types import ( + HarnessFailureTestCase, + HarnessTestCase, +) +from tests.integration.src.sqlclean.rule_testing.helpers import sample_rule + +JOINED: str = "SELECT a.id FROM a JOIN b ON a.id = b.id JOIN c ON b.id = c.id" + + +@pytest.mark.parametrize( + "test_case", + [ + HarnessTestCase( + description="the default limit reports the first join over it", + rule_name="too_many_joins", + rule_case=RuleCase(description="two joins", source=JOINED, expected_finding_count=1), + expected_texts=("JOIN c ON b.id = c.id",), + expected_fixed_source=JOINED, + ), + HarnessTestCase( + description="a configured limit replaces the default", + rule_name="too_many_joins", + rule_case=RuleCase( + description="limit two", + source=JOINED, + expected_finding_count=0, + options={"max_joins": 2}, + ), + expected_texts=(), + expected_fixed_source=JOINED, + ), + HarnessTestCase( + description="a fix after a multibyte literal edits by character offset", + rule_name="count_one", + rule_case=RuleCase( + description="count one", + source="SELECT 'é' AS mark, COUNT(1) AS n FROM t", + expected_finding_count=1, + ), + expected_texts=("1",), + expected_fixed_source="SELECT 'é' AS mark, COUNT(*) AS n FROM t", + ), + HarnessTestCase( + description="a file Rule sees every statement of the file", + rule_name="one_statement", + rule_case=RuleCase( + description="two statements", + source="SELECT 1;\nSELECT 2;\n", + expected_finding_count=1, + ), + expected_texts=("",), + expected_fixed_source="SELECT 1;\nSELECT 2;\n", + ), + HarnessTestCase( + description="a repository Rule reads the supporting files", + rule_name="single_reader", + rule_case=RuleCase( + description="orders read twice, customers once", + source="SELECT * FROM orders", + path="models/a.sql", + files=(RuleFile(path="models/b.sql", source="SELECT * FROM orders, customers"),), + expected_finding_count=1, + ), + expected_texts=("customers",), + expected_fixed_source="SELECT * FROM orders", + ), + ], + ids=lambda case: case.description, +) +def test_given_rule_case_when_evaluating_then_findings_and_fixed_source_match( + tmp_path: Path, test_case: HarnessTestCase +) -> None: + rule: RuleCheck = sample_rule(tmp_path, test_case.rule_name) + + result: RuleResult = evaluate_rule(rule=rule, test_case=test_case.rule_case) + sources: dict[str, str] = { + test_case.rule_case.path: test_case.rule_case.source, + **{file.path: file.source for file in test_case.rule_case.files}, + } + texts: tuple[str, ...] = tuple( + sources[finding.path][finding.start : finding.end] for finding in result.findings + ) + + assert texts == test_case.expected_texts + assert result.fixed_source == test_case.expected_fixed_source + + +@pytest.mark.parametrize( + "test_case", + [ + HarnessFailureTestCase( + description="a wrong expected count fails with both counts", + rule_name="too_many_joins", + rule_case=RuleCase(description="two joins", source=JOINED, expected_finding_count=0), + expected_error="expected 0 findings, got 1", + ), + HarnessFailureTestCase( + description="an option below its minimum is rejected", + rule_name="too_many_joins", + rule_case=RuleCase( + description="negative limit", + source=JOINED, + expected_finding_count=0, + options={"max_joins": -1}, + ), + expected_error="max_joins must be at least 0", + ), + HarnessFailureTestCase( + description="an undeclared option is rejected", + rule_name="too_many_joins", + rule_case=RuleCase( + description="unknown option", + source=JOINED, + expected_finding_count=0, + options={"limit": 2}, + ), + expected_error="has no option limit", + ), + HarnessFailureTestCase( + description="SQL the parser rejects fails the case", + rule_name="too_many_joins", + rule_case=RuleCase( + description="broken", source="SELECT FROM WHERE (", expected_finding_count=0 + ), + expected_error="could not parse the case SQL", + ), + ], + ids=lambda case: case.description, +) +def test_given_invalid_rule_case_when_evaluating_then_reports_the_reason( + tmp_path: Path, test_case: HarnessFailureTestCase +) -> None: + rule: RuleCheck = sample_rule(tmp_path, test_case.rule_name) + + with pytest.raises(SqlCleanError) as raised: + evaluate_rule(rule=rule, test_case=test_case.rule_case) + + assert test_case.expected_error in str(raised.value) diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/src/__init__.py b/tests/unit/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/src/sqlclean/__init__.py b/tests/unit/src/sqlclean/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/src/sqlclean/rules/__init__.py b/tests/unit/src/sqlclean/rules/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/unit/src/sqlclean/rules/_test_types.py b/tests/unit/src/sqlclean/rules/_test_types.py new file mode 100644 index 0000000..a4bc55b --- /dev/null +++ b/tests/unit/src/sqlclean/rules/_test_types.py @@ -0,0 +1,14 @@ +"""Test case types for the custom Rule authoring API.""" + +from __future__ import annotations + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class RuleCodeTestCase: + """A custom Rule code @rule must accept or reject.""" + + description: str + code: str + expected_error: str | None diff --git a/tests/unit/src/sqlclean/rules/helpers.py b/tests/unit/src/sqlclean/rules/helpers.py new file mode 100644 index 0000000..5fcd58e --- /dev/null +++ b/tests/unit/src/sqlclean/rules/helpers.py @@ -0,0 +1,12 @@ +"""A Rule body for declaration tests.""" + +from __future__ import annotations + +from sqlclean import RuleContext, Statement +from sqlclean.rules.models import Finding + + +def empty_rule(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + """Report nothing.""" + + return [] diff --git a/tests/unit/src/sqlclean/rules/test_rule_definition.py b/tests/unit/src/sqlclean/rules/test_rule_definition.py new file mode 100644 index 0000000..c10173f --- /dev/null +++ b/tests/unit/src/sqlclean/rules/test_rule_definition.py @@ -0,0 +1,69 @@ +"""Tests for @rule declarations.""" + +from __future__ import annotations + +import pytest + +from sqlclean import rule +from sqlclean.rules.constants import RULE_ATTRIBUTE +from sqlclean.rules.exceptions import RuleDefinitionError +from sqlclean.rules.types import RuleCheck +from tests.unit.src.sqlclean.rules._test_types import RuleCodeTestCase +from tests.unit.src.sqlclean.rules.helpers import empty_rule + + +@pytest.mark.parametrize( + "test_case", + [ + RuleCodeTestCase( + description="a bare custom prefix with three digits", + code="XSQC001", + expected_error=None, + ), + RuleCodeTestCase( + description="a family of capital letters", + code="XSQCNAME001", + expected_error=None, + ), + ], + ids=lambda case: case.description, +) +def test_given_custom_code_when_declaring_then_the_definition_is_attached( + test_case: RuleCodeTestCase, +) -> None: + declared: RuleCheck = rule(code=test_case.code, message="Message", remediation="Fix it.")( + empty_rule + ) + + assert getattr(declared, RULE_ATTRIBUTE).code == test_case.code + assert test_case.expected_error is None + + +@pytest.mark.parametrize( + "test_case", + [ + RuleCodeTestCase( + description="a built-in code is reserved", + code="SQC001", + expected_error="custom Rule code must be XSQC", + ), + RuleCodeTestCase( + description="a lower-case family", + code="XSQCname001", + expected_error="custom Rule code must be XSQC", + ), + RuleCodeTestCase( + description="two digits", + code="XSQCNAME01", + expected_error="custom Rule code must be XSQC", + ), + ], + ids=lambda case: case.description, +) +def test_given_invalid_code_when_declaring_then_it_is_rejected( + test_case: RuleCodeTestCase, +) -> None: + with pytest.raises(RuleDefinitionError) as raised: + rule(code=test_case.code, message="Message", remediation="Fix it.")(empty_rule) + + assert str(test_case.expected_error) in str(raised.value) From 85cf182e4bd4c7705cf0089b17d28cc1c3f73bec Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:27 +0000 Subject: [PATCH 05/29] ci: add verify, release and publish workflows Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/scripts/assert_cli_wheel_tags.sh | 30 ++ .github/scripts/release_metadata_check.py | 469 ++++++++++++++++++++ .github/workflows/publish.yml | 180 ++++++++ .github/workflows/pull-request-metadata.yml | 98 ++++ .github/workflows/release-please.yml | 272 ++++++++++++ .github/workflows/verify.yml | 105 +++++ .github/workflows/version-guard.yml | 67 +++ .pre-commit-config.yaml | 55 +++ .release-please-manifest.json | 3 + Makefile | 41 ++ release-please-config.json | 38 ++ 11 files changed, 1358 insertions(+) create mode 100644 .github/scripts/assert_cli_wheel_tags.sh create mode 100644 .github/scripts/release_metadata_check.py create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/pull-request-metadata.yml create mode 100644 .github/workflows/release-please.yml create mode 100644 .github/workflows/verify.yml create mode 100644 .github/workflows/version-guard.yml create mode 100644 .pre-commit-config.yaml create mode 100644 .release-please-manifest.json create mode 100644 Makefile create mode 100644 release-please-config.json diff --git a/.github/scripts/assert_cli_wheel_tags.sh b/.github/scripts/assert_cli_wheel_tags.sh new file mode 100644 index 0000000..bb3c99a --- /dev/null +++ b/.github/scripts/assert_cli_wheel_tags.sh @@ -0,0 +1,30 @@ +#!/usr/bin/env bash +# Guard the sqlclean-cli wheel tag. The package ships one standalone Rust +# executable that links no libpython, so an interpreter-specific tag makes the +# wheel uninstallable on every other Python and silently forces a source build. +set -euo pipefail +shopt -s nullglob + +directory="${1:?usage: assert_cli_wheel_tags.sh }" +wheels=("${directory}"/sqlclean_cli-*.whl) + +if [ ${#wheels[@]} -eq 0 ]; then + echo "No sqlclean-cli wheel found in ${directory}." + exit 1 +fi + +status=0 +for wheel in "${wheels[@]}"; do + name="$(basename "${wheel}")" + case "${name}" in + sqlclean_cli-*-py3-none-*.whl) + echo "ok: ${name}" + ;; + *) + echo "error: sqlclean-cli wheel must be tagged py3-none-: ${name}" + status=1 + ;; + esac +done + +exit "${status}" diff --git a/.github/scripts/release_metadata_check.py b/.github/scripts/release_metadata_check.py new file mode 100644 index 0000000..2d6f632 --- /dev/null +++ b/.github/scripts/release_metadata_check.py @@ -0,0 +1,469 @@ +"""Reject release branches containing non-generated metadata changes.""" + +from __future__ import annotations + +import argparse +import copy +import json +import re +import subprocess +import tomllib +from dataclasses import dataclass +from pathlib import Path +from typing import Any + +SEMVER_PART_COUNT: int = 3 +NORMALIZED_VERSION: str = "0.0.0" +CHANGELOG_PATH: str = "CHANGELOG.md" +RELEASE_MANIFEST_PATH: str = ".release-please-manifest.json" +ROOT_PROJECT_PATH: str = "pyproject.toml" +CLI_PROJECT_PATH: str = "packages/sqlclean-cli/pyproject.toml" +CARGO_WORKSPACE_PATH: str = "Cargo.toml" +CARGO_LOCK_PATH: str = "Cargo.lock" +UV_LOCK_PATH: str = "uv.lock" +CLI_DISTRIBUTION: str = "sqlclean-cli" +VERSION_PATHS: tuple[str, ...] = ( + RELEASE_MANIFEST_PATH, + ROOT_PROJECT_PATH, + CLI_PROJECT_PATH, + CARGO_WORKSPACE_PATH, + UV_LOCK_PATH, + CARGO_LOCK_PATH, +) +# Files that may not exist before the first release. +OPTIONAL_BASE_PATHS: frozenset[str] = frozenset({CHANGELOG_PATH}) +OWNED_CARGO_PACKAGES: frozenset[str] = frozenset({"sqlclean", "sqlclean-cli"}) +OWNED_UV_PACKAGES: frozenset[str] = frozenset({"sqlclean", "sqlclean-cli"}) +ALLOWED_PATHS: frozenset[str] = frozenset((*VERSION_PATHS, CHANGELOG_PATH)) +# The TOML table holding the owned version field in each manifest. +MANIFEST_SECTIONS: dict[str, tuple[str, ...]] = { + ROOT_PROJECT_PATH: ("project",), + CLI_PROJECT_PATH: ("project",), + CARGO_WORKSPACE_PATH: ("workspace", "package"), +} + + +@dataclass(frozen=True) +class ReleaseDelta: + """Base and release-head content for generated metadata files.""" + + old_version: str + new_version: str + changed_paths: frozenset[str] + base_files: dict[str, str] + head_files: dict[str, str] + + +def load_release_delta(*, repo_root: Path, base_ref: str, head_ref: str) -> ReleaseDelta: + """Read the changed metadata and package versions from two Git revisions.""" + + changed: list[str] = _git( + "diff", "--name-only", base_ref, head_ref, repo_root=repo_root + ).splitlines() + base_files: dict[str, str] = { + path: _show(ref=base_ref, path=path, repo_root=repo_root) for path in ALLOWED_PATHS + } + head_files: dict[str, str] = { + path: _git("show", f"{head_ref}:{path}", repo_root=repo_root) for path in ALLOWED_PATHS + } + old_version: str = tomllib.loads(base_files[ROOT_PROJECT_PATH])["project"]["version"] + new_version: str = tomllib.loads(head_files[ROOT_PROJECT_PATH])["project"]["version"] + return ReleaseDelta( + old_version=old_version, + new_version=new_version, + changed_paths=frozenset(changed), + base_files=base_files, + head_files=head_files, + ) + + +def get_release_delta_errors( + *, + delta: ReleaseDelta, + allow_stale_lockfiles: bool = False, +) -> list[str]: + """Return every unsafe or inconsistent release delta.""" + + errors: list[str] = [] + unexpected: list[str] = sorted(delta.changed_paths - ALLOWED_PATHS) + if unexpected: + errors.append(f"unexpected release files: {', '.join(unexpected)}") + if not is_forward_semver(old_version=delta.old_version, new_version=delta.new_version): + errors.append("release version must be a forward MAJOR.MINOR.PATCH transition") + for path in VERSION_PATHS: + if not version_file_is_valid( + path=path, + delta=delta, + allow_stale_lockfiles=allow_stale_lockfiles, + ): + errors.append(f"{path} contains changes beyond the release version substitution") + changelog: str = delta.head_files[CHANGELOG_PATH] + base_history: str = delta.base_files[CHANGELOG_PATH].partition("\n\n")[2] + if not changelog.startswith("# Changelog\n\n") or base_history not in changelog: + errors.append("CHANGELOG.md rewrites existing release history") + if f"## [{delta.new_version}]" not in changelog: + errors.append(f"CHANGELOG.md has no {delta.new_version} release heading") + return errors + + +def is_forward_semver(*, old_version: str, new_version: str) -> bool: + """Return whether two plain semantic versions form a forward transition.""" + + old_semver: tuple[int, int, int] | None = _parse_semver(value=old_version) + new_semver: tuple[int, int, int] | None = _parse_semver(value=new_version) + return old_semver is not None and new_semver is not None and new_semver > old_semver + + +def version_file_is_valid( + *, + path: str, + delta: ReleaseDelta, + allow_stale_lockfiles: bool, +) -> bool: + """Compare one parsed metadata file after normalizing owned version fields.""" + + try: + semantic_valid: bool + if path == RELEASE_MANIFEST_PATH: + semantic_valid = _release_manifest_is_valid(delta=delta, path=path) + else: + base: dict[str, Any] = tomllib.loads(delta.base_files[path]) + head: dict[str, Any] = tomllib.loads(delta.head_files[path]) + if path == ROOT_PROJECT_PATH: + semantic_valid = _root_project_is_valid(base=base, head=head, delta=delta) + elif path in MANIFEST_SECTIONS: + semantic_valid = _manifest_is_valid( + base=base, + head=head, + section=MANIFEST_SECTIONS[path], + delta=delta, + ) + elif path == CARGO_LOCK_PATH: + semantic_valid = _lock_is_valid( + base=base, + head=head, + owned_packages=OWNED_CARGO_PACKAGES, + delta=delta, + allow_old=allow_stale_lockfiles, + ) + elif path == UV_LOCK_PATH: + semantic_valid = _lock_is_valid( + base=base, + head=head, + owned_packages=OWNED_UV_PACKAGES, + delta=delta, + allow_old=allow_stale_lockfiles, + ) + else: + return False + return semantic_valid and _version_text_is_valid( + path=path, + delta=delta, + allow_stale_lockfiles=allow_stale_lockfiles, + ) + except (KeyError, TypeError, ValueError, tomllib.TOMLDecodeError): + return False + + +def _parse_semver(*, value: str) -> tuple[int, int, int] | None: + parts: list[str] = value.split(".") + if len(parts) != SEMVER_PART_COUNT or any(not part.isdigit() for part in parts): + return None + return (int(parts[0]), int(parts[1]), int(parts[2])) + + +def _release_manifest_is_valid(*, delta: ReleaseDelta, path: str) -> bool: + base: dict[str, Any] = json.loads(delta.base_files[path]) + head: dict[str, Any] = json.loads(delta.head_files[path]) + base_normalized: dict[str, Any] | None = _normalized_field( + document=base, + section=(), + field=".", + expected=delta.old_version, + ) + head_normalized: dict[str, Any] | None = _normalized_field( + document=head, + section=(), + field=".", + expected=delta.new_version, + ) + return base_normalized is not None and base_normalized == head_normalized + + +def _manifest_is_valid( + *, + base: dict[str, Any], + head: dict[str, Any], + section: tuple[str, ...], + delta: ReleaseDelta, +) -> bool: + base_normalized: dict[str, Any] | None = _normalized_field( + document=base, + section=section, + field="version", + expected=delta.old_version, + ) + head_normalized: dict[str, Any] | None = _normalized_field( + document=head, + section=section, + field="version", + expected=delta.new_version, + ) + return base_normalized is not None and base_normalized == head_normalized + + +def _root_project_is_valid( + *, + base: dict[str, Any], + head: dict[str, Any], + delta: ReleaseDelta, +) -> bool: + base_normalized: dict[str, Any] | None = _normalized_field( + document=base, + section=("project",), + field="version", + expected=delta.old_version, + ) + head_normalized: dict[str, Any] | None = _normalized_field( + document=head, + section=("project",), + field="version", + expected=delta.new_version, + ) + if base_normalized is None or head_normalized is None: + return False + base_normalized = _normalized_cli_dependency( + document=base_normalized, + version=delta.old_version, + ) + head_normalized = _normalized_cli_dependency( + document=head_normalized, + version=delta.new_version, + ) + return base_normalized is not None and base_normalized == head_normalized + + +def _normalized_field( + *, + document: dict[str, Any], + section: tuple[str, ...], + field: str, + expected: str, +) -> dict[str, Any] | None: + normalized: dict[str, Any] = copy.deepcopy(document) + target: dict[str, Any] = normalized + for key in section: + target = target[key] + if target.get(field) != expected: + return None + target[field] = NORMALIZED_VERSION + return normalized + + +def _normalized_cli_dependency( + *, + document: dict[str, Any], + version: str, +) -> dict[str, Any] | None: + requirement: str = f"{CLI_DISTRIBUTION}=={version}" + dependencies: list[Any] = document["project"].get("dependencies", []) + if dependencies.count(requirement) != 1: + return None + normalized: dict[str, Any] = copy.deepcopy(document) + normalized["project"]["dependencies"] = [ + f"{CLI_DISTRIBUTION}=={NORMALIZED_VERSION}" if item == requirement else item + for item in dependencies + ] + return normalized + + +def _lock_is_valid( + *, + base: dict[str, Any], + head: dict[str, Any], + owned_packages: frozenset[str], + delta: ReleaseDelta, + allow_old: bool, +) -> bool: + base_normalized: dict[str, Any] | None = _normalized_lock( + document=base, + owned_packages=owned_packages, + allowed_versions=frozenset({delta.old_version}), + ) + allowed_head: frozenset[str] = frozenset( + {delta.old_version, delta.new_version} if allow_old else {delta.new_version} + ) + head_normalized: dict[str, Any] | None = _normalized_lock( + document=head, + owned_packages=owned_packages, + allowed_versions=allowed_head, + ) + return base_normalized is not None and base_normalized == head_normalized + + +def _normalized_lock( + *, + document: dict[str, Any], + owned_packages: frozenset[str], + allowed_versions: frozenset[str], +) -> dict[str, Any] | None: + normalized: dict[str, Any] = copy.deepcopy(document) + found: set[str] = set() + for package in normalized.get("package", []): + if package.get("name") not in owned_packages: + continue + found.add(package["name"]) + if package.get("version") not in allowed_versions: + return None + package["version"] = NORMALIZED_VERSION + return normalized if found == owned_packages else None + + +def _version_text_is_valid( + *, + path: str, + delta: ReleaseDelta, + allow_stale_lockfiles: bool, +) -> bool: + base: str = delta.base_files[path] + expected: str | None + if path == RELEASE_MANIFEST_PATH: + expected = _replace_once( + value=base, + pattern=rf'("\."\s*:\s*"){re.escape(delta.old_version)}(")', + replacement=rf"\g<1>{delta.new_version}\g<2>", + ) + elif path in MANIFEST_SECTIONS: + expected = _replace_manifest_version( + value=base, + section=".".join(MANIFEST_SECTIONS[path]), + old_version=delta.old_version, + new_version=delta.new_version, + ) + if expected is not None and path == ROOT_PROJECT_PATH: + expected = _replace_once( + value=expected, + pattern=re.escape(f"{CLI_DISTRIBUTION}=={delta.old_version}"), + replacement=f"{CLI_DISTRIBUTION}=={delta.new_version}", + ) + elif path == CARGO_LOCK_PATH: + expected = _replace_lock_versions( + value=base, + owned_packages=OWNED_CARGO_PACKAGES, + old_version=delta.old_version, + new_version=delta.new_version, + ) + elif path == UV_LOCK_PATH: + expected = _replace_lock_versions( + value=base, + owned_packages=OWNED_UV_PACKAGES, + old_version=delta.old_version, + new_version=delta.new_version, + ) + else: + return False + if expected is None: + return False + head: str = delta.head_files[path] + return head == expected or ( + allow_stale_lockfiles and path in {CARGO_LOCK_PATH, UV_LOCK_PATH} and head == base + ) + + +def _replace_manifest_version( + *, + value: str, + section: str, + old_version: str, + new_version: str, +) -> str | None: + return _replace_once( + value=value, + pattern=( + rf'(?ms)(^\[{re.escape(section)}\]\s*$.*?^version\s*=\s*")' + rf"{re.escape(old_version)}" + r'("[^\n]*$)' + ), + replacement=rf"\g<1>{new_version}\g<2>", + ) + + +def _replace_lock_versions( + *, + value: str, + owned_packages: frozenset[str], + old_version: str, + new_version: str, +) -> str | None: + blocks: list[str] = re.split(r"(?=^\[\[package\]\]\s*$)", value, flags=re.MULTILINE) + found: set[str] = set() + for index, block in enumerate(blocks): + name_match: re.Match[str] | None = re.search(r'^name\s*=\s*"([^"]+)"', block, re.MULTILINE) + if name_match is None or name_match.group(1) not in owned_packages: + continue + found.add(name_match.group(1)) + replaced: str | None = _replace_once( + value=block, + pattern=rf'(?m)(^version\s*=\s*"){re.escape(old_version)}("[^\n]*$)', + replacement=rf"\g<1>{new_version}\g<2>", + ) + if replaced is None: + return None + blocks[index] = replaced + return "".join(blocks) if found == owned_packages else None + + +def _replace_once(*, value: str, pattern: str, replacement: str) -> str | None: + replaced, count = re.subn(pattern, replacement, value, count=1) + return replaced if count == 1 else None + + +def _show(*, ref: str, path: str, repo_root: Path) -> str: + try: + return _git("show", f"{ref}:{path}", repo_root=repo_root) + except subprocess.CalledProcessError: + if path in OPTIONAL_BASE_PATHS: + return "" + raise + + +def _git(*arguments: str, repo_root: Path) -> str: + result: subprocess.CompletedProcess[str] = subprocess.run( + ["git", *arguments], + cwd=repo_root, + check=True, + capture_output=True, + text=True, + ) + return result.stdout + + +def _parse_args() -> argparse.Namespace: + parser: argparse.ArgumentParser = argparse.ArgumentParser(prog="release_metadata_check") + parser.add_argument("--base", required=True) + parser.add_argument("--head", default="HEAD") + parser.add_argument("--repo", type=Path, default=Path.cwd()) + parser.add_argument("--allow-stale-lockfiles", action="store_true") + return parser.parse_args() + + +def main() -> int: + """Validate one generated release delta and render actionable failures.""" + + arguments: argparse.Namespace = _parse_args() + delta: ReleaseDelta = load_release_delta( + repo_root=arguments.repo, + base_ref=arguments.base, + head_ref=arguments.head, + ) + errors: list[str] = get_release_delta_errors( + delta=delta, + allow_stale_lockfiles=arguments.allow_stale_lockfiles, + ) + for error in errors: + print(f"release metadata error: {error}") + return int(bool(errors)) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..7de9a50 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,180 @@ +name: Publish + +on: + workflow_dispatch: + inputs: + tag: + description: "Release tag to publish, for example v0.1.0" + required: true + type: string + +permissions: + contents: read + +jobs: + release-tag: + name: Resolve release tag + runs-on: ubuntu-latest + outputs: + tag: ${{ steps.release-tag.outputs.tag }} + version: ${{ steps.release-tag.outputs.version }} + steps: + - name: Resolve release tag + id: release-tag + run: | + tag="${{ inputs.tag }}" + + if [[ ! "$tag" =~ ^v0\.[0-9]+\.[0-9]+$ ]]; then + echo "Refusing to publish tag '$tag'. Expected exact v0.MINOR.PATCH semver." + exit 1 + fi + + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo "version=${tag#v}" >> "$GITHUB_OUTPUT" + + release-metadata: + name: Validate release metadata + needs: release-tag + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.release-tag.outputs.tag }} + - name: Match package versions to release tag + env: + RELEASE_VERSION: ${{ needs.release-tag.outputs.version }} + run: | + python - <<'PY' + import os + import tomllib + from pathlib import Path + + expected = os.environ["RELEASE_VERSION"] + with Path("pyproject.toml").open("rb") as file: + root = tomllib.load(file)["project"] + cli_dependency = next( + item for item in root["dependencies"] if item.startswith("sqlclean-cli==") + ).partition("==")[2] + with Path("packages/sqlclean-cli/pyproject.toml").open("rb") as file: + cli_package_version = tomllib.load(file)["project"]["version"] + with Path("Cargo.toml").open("rb") as file: + workspace_version = tomllib.load(file)["workspace"]["package"]["version"] + manifests = [ + ("root project", root["version"]), + ("root CLI dependency", cli_dependency), + ("CLI package", cli_package_version), + ("Cargo workspace", workspace_version), + ] + mismatches = [f"{label}={version}" for label, version in manifests if version != expected] + if mismatches: + raise SystemExit( + f"Release tag version {expected} does not match: {', '.join(mismatches)}" + ) + PY + + wheels: + name: Build wheel (${{ matrix.platform.name }}) + needs: [release-tag, release-metadata] + runs-on: ${{ matrix.platform.runner }} + strategy: + fail-fast: false + matrix: + platform: + - name: linux-x86_64 + runner: ubuntu-latest + target: x86_64 + - name: linux-aarch64 + runner: ubuntu-24.04-arm + target: aarch64 + - name: macos-x86_64 + runner: macos-latest + target: x86_64 + - name: macos-arm64 + runner: macos-latest + target: aarch64 + - name: windows-x86_64 + runner: windows-latest + target: x86_64 + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.release-tag.outputs.tag }} + - uses: actions/setup-python@v6 + with: + python-version: "3.12" + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + # sqlclean-cli ships one standalone Rust executable and links no libpython, + # so maturin tags it py3-none-. The tag is asserted before release. + - name: Build native CLI wheel + uses: PyO3/maturin-action@v1 + with: + command: build + args: --release --out ../../dist + target: ${{ matrix.platform.target }} + manylinux: auto + working-directory: packages/sqlclean-cli + - name: Assert native CLI wheel is interpreter-agnostic + shell: bash + run: bash .github/scripts/assert_cli_wheel_tags.sh dist + - uses: actions/upload-artifact@v5 + with: + name: wheel-${{ matrix.platform.name }} + path: dist + + sdist: + name: Build sdists and Python wheel + needs: [release-tag, release-metadata] + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.release-tag.outputs.tag }} + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + - uses: astral-sh/setup-uv@v6 + # The sqlclean package is pure Python, so one wheel serves every platform. + - name: Build Python package sdist and wheel + run: uv build --sdist --wheel --out-dir dist + - name: Build native CLI sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out ../../dist + working-directory: packages/sqlclean-cli + - uses: actions/upload-artifact@v5 + with: + name: sdist + path: dist + + pypi: + name: Publish to PyPI + needs: + - release-tag + - wheels + - sdist + runs-on: ubuntu-latest + environment: pypi + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v5 + with: + ref: ${{ needs.release-tag.outputs.tag }} + - uses: actions/download-artifact@v5 + with: + path: dist + merge-multiple: true + + - name: Assert native CLI wheels are interpreter-agnostic + shell: bash + run: bash .github/scripts/assert_cli_wheel_tags.sh dist + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist + skip-existing: true diff --git a/.github/workflows/pull-request-metadata.yml b/.github/workflows/pull-request-metadata.yml new file mode 100644 index 0000000..64889d1 --- /dev/null +++ b/.github/workflows/pull-request-metadata.yml @@ -0,0 +1,98 @@ +name: Pull Request Metadata + +on: + pull_request: + types: [opened, edited, synchronize, reopened] + workflow_dispatch: + inputs: + pr_number: + description: Pull request number to validate + required: true + type: number + +permissions: + contents: read + pull-requests: read + +jobs: + validate: + name: Pull Request Metadata + runs-on: ubuntu-latest + steps: + - name: Resolve pull request metadata + env: + EVENT_NAME: ${{ github.event_name }} + GH_TOKEN: ${{ github.token }} + PR_NUMBER: ${{ inputs.pr_number }} + run: | + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + cp "${GITHUB_EVENT_PATH}" "${RUNNER_TEMP}/pull-request.json" + else + gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" \ + > "${RUNNER_TEMP}/pull-request.json" + fi + + - name: Validate branch, title, and description + env: + PR_METADATA_PATH: ${{ runner.temp }}/pull-request.json + run: | + python - <<'PY' + import json + import os + import re + import sys + + with open(os.environ["PR_METADATA_PATH"], encoding="utf-8") as metadata_file: + payload = json.load(metadata_file) + pull_request = payload.get("pull_request", payload) + branch = pull_request["head"]["ref"] + title = pull_request["title"] + body = pull_request.get("body") or "" + + branch_pattern = re.compile( + r"^(feat|fix|perf|refactor|test|docs|build|ci|chore|revert)/" + r"[a-z0-9]+(?:-[a-z0-9]+)*$" + ) + automation_pattern = re.compile( + r"^(release-please--|dependabot/|renovate/|github-actions/)" + ) + title_pattern = re.compile( + r"^(feat|fix|perf|refactor|test|docs|build|ci|chore|revert)" + r"(?:\([a-z0-9][a-z0-9-]*\))?!?: [^ ].+$" + ) + + errors: list[str] = [] + automated = automation_pattern.match(branch) is not None + if not automated and branch_pattern.fullmatch(branch) is None: + errors.append("Branch must use /.") + if title_pattern.fullmatch(title) is None: + errors.append("PR title must follow Conventional Commits.") + + if not automated: + if len(body) > 2_000: + errors.append("PR description must not exceed 2,000 characters.") + sections = list( + re.finditer(r"^## (Why|Changes|Verification)\s*$", body, re.MULTILINE) + ) + found = [match.group(1) for match in sections] + expected = ["Why", "Changes", "Verification"] + if found != expected: + errors.append( + "PR description must contain Why, Changes, and Verification " + "sections in that order." + ) + else: + for index, match in enumerate(sections): + end = sections[index + 1].start() if index + 1 < len(sections) else len(body) + content = re.sub(r"", "", body[match.end():end], flags=re.DOTALL) + if not content.strip(): + errors.append(f"PR description section {match.group(1)} is empty.") + + if errors: + print(f"Invalid pull request metadata for {branch}:", file=sys.stderr) + for error in errors: + print(f"- {error}", file=sys.stderr) + sys.exit(1) + + print(f"Pull request metadata is valid for {branch}.") + PY diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml new file mode 100644 index 0000000..8d3bf9e --- /dev/null +++ b/.github/workflows/release-please.yml @@ -0,0 +1,272 @@ +name: Release Please + +on: + push: + branches: + - main + workflow_dispatch: + +permissions: + actions: write + contents: write + pull-requests: write + statuses: write + +concurrency: + group: release-please + cancel-in-progress: false + +jobs: + release-please: + runs-on: ubuntu-latest + steps: + - name: Create delivery token + id: delivery-token + uses: actions/create-github-app-token@v2 + with: + app-id: ${{ vars.RELEASE_APP_ID }} + private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: ${{ github.event.repository.name }} + permission-actions: write + permission-contents: write + permission-issues: write + permission-pull-requests: write + + - uses: googleapis/release-please-action@v4 + id: release + with: + token: ${{ steps.delivery-token.outputs.token }} + config-file: release-please-config.json + manifest-file: .release-please-manifest.json + + - name: Publish created release + if: steps.release.outputs.release_created == 'true' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ steps.release.outputs.tag_name }} + run: | + gh workflow run publish.yml --repo "${GITHUB_REPOSITORY}" --ref main \ + -f "tag=${RELEASE_TAG}" + + - name: Resolve release pull request + id: release_pr + if: steps.release.outputs.prs_created == 'true' + env: + GH_TOKEN: ${{ github.token }} + RELEASE_PRS: ${{ steps.release.outputs.prs }} + run: | + number="$(jq -r '.[0].number // empty' <<< "${RELEASE_PRS}")" + branch="$(jq -r '.[0].headBranchName // empty' <<< "${RELEASE_PRS}")" + if [[ -z "${number}" || -z "${branch}" ]]; then + echo "Release Please did not return a pull request number and branch" >&2 + exit 1 + fi + pr_json="$(gh pr view "${number}" --repo "${GITHUB_REPOSITORY}" \ + --json baseRefName,baseRefOid,headRefName)" + if [[ "$(jq -r .baseRefName <<< "${pr_json}")" != "main" \ + || "$(jq -r .headRefName <<< "${pr_json}")" != "${branch}" ]]; then + echo "Release pull request branches do not match Release Please output" >&2 + exit 1 + fi + head_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/${branch}" \ + --jq .object.sha)" + echo "number=${number}" >> "${GITHUB_OUTPUT}" + echo "branch=${branch}" >> "${GITHUB_OUTPUT}" + echo "base_sha=$(jq -r .baseRefOid <<< "${pr_json}")" >> "${GITHUB_OUTPUT}" + echo "head_sha=${head_sha}" >> "${GITHUB_OUTPUT}" + + - uses: actions/checkout@v5 + if: steps.release.outputs.prs_created == 'true' + with: + ref: ${{ steps.release_pr.outputs.base_sha }} + path: trusted-base + persist-credentials: false + + - uses: actions/checkout@v5 + if: steps.release.outputs.prs_created == 'true' + with: + ref: ${{ steps.release_pr.outputs.head_sha }} + path: release-head + fetch-depth: 0 + token: ${{ steps.delivery-token.outputs.token }} + + - name: Validate release metadata and source provenance + if: steps.release.outputs.prs_created == 'true' + working-directory: trusted-base + env: + BASE_SHA: ${{ steps.release_pr.outputs.base_sha }} + GH_TOKEN: ${{ github.token }} + HEAD_SHA: ${{ steps.release_pr.outputs.head_sha }} + run: | + python .github/scripts/release_metadata_check.py \ + --repo ../release-head --base "${BASE_SHA}" --head "${HEAD_SHA}" \ + --allow-stale-lockfiles + + source_pr_json="$(gh api "repos/${GITHUB_REPOSITORY}/commits/${BASE_SHA}/pulls" \ + --jq '[.[] | select(.merged_at != null and .base.ref == "main")] | sort_by(.merged_at) | last')" + source_pr="$(jq -r '.number // empty' <<< "${source_pr_json}")" + source_head="$(jq -r '.head.sha // empty' <<< "${source_pr_json}")" + if [[ -z "${source_pr}" || -z "${source_head}" ]]; then + echo "Release base ${BASE_SHA} is not associated with a pull request merged to main" >&2 + exit 1 + fi + base_tree="$(gh api "repos/${GITHUB_REPOSITORY}/git/commits/${BASE_SHA}" --jq .tree.sha)" + verified_tree="$(gh api "repos/${GITHUB_REPOSITORY}/git/commits/${source_head}" --jq .tree.sha)" + if [[ "${base_tree}" != "${verified_tree}" ]]; then + echo "Release base tree was not verified on source pull request ${source_pr}" >&2 + exit 1 + fi + successful_runs="$(gh api \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/verify.yml/runs?head_sha=${source_head}&event=pull_request&status=completed&per_page=100" \ + | jq --arg source_head "${source_head}" \ + '[.workflow_runs[] | select(.conclusion == "success" and .head_sha == $source_head)] | length')" + if [[ "${successful_runs}" -lt 1 ]]; then + echo "No successful Verify run belongs to source pull request ${source_pr}" >&2 + exit 1 + fi + + - uses: astral-sh/setup-uv@v6 + if: steps.release.outputs.prs_created == 'true' + + - uses: dtolnay/rust-toolchain@master + if: steps.release.outputs.prs_created == 'true' + with: + toolchain: 1.95.0 + + - name: Refresh release lockfiles + if: steps.release.outputs.prs_created == 'true' + working-directory: release-head + env: + RELEASE_BRANCH: ${{ steps.release_pr.outputs.branch }} + run: | + uv lock + cargo check --workspace + if git diff --quiet -- uv.lock Cargo.lock; then + exit 0 + fi + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git add uv.lock Cargo.lock + git commit -m "chore: refresh release lockfiles" + git push origin "HEAD:${RELEASE_BRANCH}" + + - name: Validate refreshed release metadata + if: steps.release.outputs.prs_created == 'true' + working-directory: trusted-base + env: + BASE_SHA: ${{ steps.release_pr.outputs.base_sha }} + run: | + python .github/scripts/release_metadata_check.py \ + --repo ../release-head --base "${BASE_SHA}" --head HEAD + + - name: Run required release checks + if: steps.release.outputs.prs_created == 'true' + working-directory: release-head + env: + GH_TOKEN: ${{ github.token }} + RELEASE_BRANCH: ${{ steps.release_pr.outputs.branch }} + RELEASE_PR: ${{ steps.release_pr.outputs.number }} + run: | + head_sha="$(git rev-parse HEAD)" + remote_head="$(gh api \ + "repos/${GITHUB_REPOSITORY}/git/ref/heads/${RELEASE_BRANCH}" \ + --jq .object.sha)" + if [[ "${remote_head}" != "${head_sha}" ]]; then + echo "Release pull request moved from ${head_sha} to ${remote_head}" >&2 + exit 1 + fi + echo "head_sha=${head_sha}" >> "${GITHUB_OUTPUT}" + workflows=(pull-request-metadata.yml version-guard.yml verify.yml) + metadata_contexts=("Pull Request Metadata") + version_contexts=("Block automatic 1.0 releases") + verify_contexts=( + "verify" + "rust" + "Minimum supported Rust (1.88)" + "windows" + "native-package-smoke (ubuntu-latest)" + "native-package-smoke (macos-latest)" + "native-package-smoke (windows-latest)" + "source-build" + ) + required_contexts=( + "${metadata_contexts[@]}" + "${version_contexts[@]}" + "${verify_contexts[@]}" + ) + fail_required_contexts() { + status=$? + set +e + run_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" + for context in "${required_contexts[@]}"; do + gh api --method POST "repos/${GITHUB_REPOSITORY}/statuses/${head_sha}" \ + -f state=failure -f "context=${context}" -f "target_url=${run_url}" \ + -f description="Trusted release orchestration failed" >/dev/null + done + exit "${status}" + } + trap fail_required_contexts EXIT + + declare -A previous_run_ids + for workflow in "${workflows[@]}"; do + previous_run_ids["${workflow}"]="$(gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \ + -f "branch=${RELEASE_BRANCH}" -f event=workflow_dispatch -f per_page=100 \ + --jq '[.workflow_runs[].id] | max // 0')" + done + for context in "${required_contexts[@]}"; do + gh api --method POST "repos/${GITHUB_REPOSITORY}/statuses/${head_sha}" \ + -f state=pending -f "context=${context}" \ + -f description="Trusted release check is running" >/dev/null + done + + gh workflow run pull-request-metadata.yml --repo "${GITHUB_REPOSITORY}" \ + --ref "${RELEASE_BRANCH}" -f "pr_number=${RELEASE_PR}" + gh workflow run version-guard.yml --repo "${GITHUB_REPOSITORY}" \ + --ref "${RELEASE_BRANCH}" + gh workflow run verify.yml --repo "${GITHUB_REPOSITORY}" \ + --ref "${RELEASE_BRANCH}" + + for workflow in "${workflows[@]}"; do + run_id="" + for _ in {1..30}; do + runs_json="$(gh api --method GET \ + "repos/${GITHUB_REPOSITORY}/actions/workflows/${workflow}/runs" \ + -f "branch=${RELEASE_BRANCH}" -f event=workflow_dispatch -f per_page=100)" + run_id="$(jq -r --arg sha "${head_sha}" \ + --argjson previous "${previous_run_ids[${workflow}]}" \ + '[.workflow_runs[] | select( + .id > $previous + and .head_sha == $sha + and .actor.login == "github-actions[bot]" + )] | sort_by(.id) | first | .id // empty' <<< "${runs_json}")" + [[ -n "${run_id}" ]] && break + sleep 2 + done + if [[ -z "${run_id}" ]]; then + echo "Required workflow ${workflow} did not start for ${head_sha}" >&2 + exit 1 + fi + run_url="$(gh run view "${run_id}" --repo "${GITHUB_REPOSITORY}" \ + --json url --jq .url)" + if gh run watch "${run_id}" --repo "${GITHUB_REPOSITORY}" --exit-status; then + state=success + description="Trusted release check passed" + else + state=failure + description="Trusted release check failed" + fi + case "${workflow}" in + pull-request-metadata.yml) contexts=("${metadata_contexts[@]}") ;; + version-guard.yml) contexts=("${version_contexts[@]}") ;; + verify.yml) contexts=("${verify_contexts[@]}") ;; + esac + for context in "${contexts[@]}"; do + gh api --method POST "repos/${GITHUB_REPOSITORY}/statuses/${head_sha}" \ + -f "state=${state}" -f "context=${context}" -f "target_url=${run_url}" \ + -f "description=${description}" >/dev/null + done + [[ "${state}" == "success" ]] || exit 1 + done + trap - EXIT diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..8068a2f --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,105 @@ +name: Verify + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: verify-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: uv python install 3.12 + - run: uv sync --locked --all-groups + - run: make verify + + rust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + components: rustfmt, clippy + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 + - run: uv sync --locked --all-groups + - run: make check-rust + - run: make test-rust + + minimum-rust: + name: Minimum supported Rust (1.88) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.88.0 + - uses: Swatinem/rust-cache@v2 + - run: cargo +1.88.0 check --workspace --all-targets --locked + + windows: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true + - run: uv python install 3.12 + - run: uv sync --locked --all-groups + - run: cargo test --workspace --locked + - run: uv run pytest tests -q -n auto + + native-package-smoke: + strategy: + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + runs-on: ${{ matrix.os }} + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 + - run: uv sync --locked --all-groups + - run: uv run sqlclean --version + - run: uv run pytest tests/e2e -q + + source-build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@master + with: + toolchain: 1.95.0 + - uses: Swatinem/rust-cache@v2 + - uses: astral-sh/setup-uv@v6 + - run: uv python install 3.12 + - run: uv build --sdist --out-dir dist + - run: uvx maturin sdist --out ../../dist + working-directory: packages/sqlclean-cli + - run: uv venv .source-venv --python 3.12 + - run: uv pip install --python .source-venv/bin/python --no-deps dist/*.tar.gz + - run: .source-venv/bin/sqlclean --version diff --git a/.github/workflows/version-guard.yml b/.github/workflows/version-guard.yml new file mode 100644 index 0000000..fb521f6 --- /dev/null +++ b/.github/workflows/version-guard.yml @@ -0,0 +1,67 @@ +name: Version Guard + +on: + pull_request: + branches: + - main + push: + branches: + - main + workflow_dispatch: + +permissions: + contents: read + +jobs: + block-1-0: + name: Block automatic 1.0 releases + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Reject versions at or above 1.0.0 + run: | + python - <<'PY' + import sys + import tomllib + from pathlib import Path + + with open("pyproject.toml", "rb") as pyproject: + project = tomllib.load(pyproject)["project"] + version = project["version"] + + cli_dependency = next( + item for item in project["dependencies"] if item.startswith("sqlclean-cli==") + ).partition("==")[2] + with Path("packages/sqlclean-cli/pyproject.toml").open("rb") as pyproject: + cli_package_version = tomllib.load(pyproject)["project"]["version"] + with Path("Cargo.toml").open("rb") as cargo: + workspace_version = tomllib.load(cargo)["workspace"]["package"]["version"] + + versions = { + version, + cli_dependency, + cli_package_version, + workspace_version, + } + if len(versions) != 1: + print(f"SQLClean package versions are not locked: {sorted(versions)}", file=sys.stderr) + sys.exit(1) + + parts = version.split(".") + try: + major = int(parts[0]) + except (IndexError, ValueError): + print(f"Invalid project version: {version}", file=sys.stderr) + sys.exit(1) + + if major >= 1: + print( + f"Refusing automatic release version {version}. " + "Crossing to 1.0.0 requires an explicit manual decision.", + file=sys.stderr, + ) + sys.exit(1) + + print(f"Project version {version} is below 1.0.0.") + PY diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..062da2d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +default_stages: [pre-commit] + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-toml + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + + - repo: local + hooks: + - id: ruff-format + name: ruff format + entry: uv run ruff format + language: system + types_or: [python, pyi] + + - id: ruff-check + name: ruff check + entry: uv run ruff check --fix + language: system + types_or: [python, pyi] + + - id: ty-check + name: ty check + entry: bash -c 'uv run ty check src tests' + language: system + pass_filenames: false + + - id: fensu-check + name: fensu check + entry: bash -c 'uv run fensu check' + language: system + pass_filenames: false + + - id: cargo-fmt + name: cargo fmt + entry: cargo fmt --check + language: system + types: [rust] + pass_filenames: false + + - id: pytest + name: pytest + entry: bash -c 'uv run pytest tests/unit tests/integration -q -n auto' + language: system + pass_filenames: false + + - id: conventional-commit + name: conventional commit message + entry: uv run cz check --commit-msg-file + language: system + stages: [commit-msg] diff --git a/.release-please-manifest.json b/.release-please-manifest.json new file mode 100644 index 0000000..b985ff6 --- /dev/null +++ b/.release-please-manifest.json @@ -0,0 +1,3 @@ +{ + ".": "0.0.1" +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..50dc0cc --- /dev/null +++ b/Makefile @@ -0,0 +1,41 @@ +SHELL := /bin/bash + +.PHONY: check check-ci check-rust develop-native test test-e2e test-integration test-rust test-unit verify + +check: + uv run ruff format . + uv run ruff check --fix . + uv run ty check src tests + @if command -v cargo >/dev/null; then uv run fensu check; else uv run fensu check --target python; fi + @command -v cargo >/dev/null && $(MAKE) --no-print-directory check-rust || true + +check-ci: + uv run ruff format --check . + uv run ruff check . + uv run ty check src tests + uv run fensu check + $(MAKE) --no-print-directory check-rust + +check-rust: + cargo fmt --check + cargo clippy --all-targets --quiet -- -D warnings + +develop-native: + uv sync --all-groups --reinstall-package sqlclean-cli + +test: test-rust + uv run pytest tests -q -n auto + +test-rust: + cargo test --all --quiet + +test-unit: + uv run pytest tests/unit -q -n auto + +test-integration: + uv run pytest tests/integration -q -n auto + +test-e2e: + uv run pytest tests/e2e -q -n auto + +verify: check-ci test diff --git a/release-please-config.json b/release-please-config.json new file mode 100644 index 0000000..c483077 --- /dev/null +++ b/release-please-config.json @@ -0,0 +1,38 @@ +{ + "packages": { + ".": { + "release-type": "python", + "package-name": "sqlclean", + "include-v-in-tag": true, + "include-component-in-tag": false, + "bump-minor-pre-major": true, + "bump-patch-for-minor-pre-major": false, + "changelog-sections": [ + { "type": "feat", "section": "Features" }, + { "type": "fix", "section": "Bug Fixes" }, + { "type": "revert", "section": "Bug Fixes" }, + { "type": "perf", "section": "Performance Improvements" }, + { "type": "refactor", "section": "Refactoring" }, + { "type": "build", "section": "Build System" }, + { "type": "docs", "section": "Documentation" }, + { "type": "chore", "section": "Maintenance" }, + { "type": "ci", "section": "Continuous Integration", "hidden": true }, + { "type": "test", "section": "Tests", "hidden": true } + ], + "extra-files": [ + { + "type": "generic", + "path": "pyproject.toml" + }, + { + "type": "generic", + "path": "Cargo.toml" + }, + { + "type": "generic", + "path": "packages/sqlclean-cli/pyproject.toml" + } + ] + } + } +} From 303c89f1988422804dc9f75d218813b9e4f242b6 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 08:07:27 +0000 Subject: [PATCH 06/29] docs: document commands, configuration and custom Rules Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- AGENTS.md | 32 ++++++++ CHANGELOG.md | 1 + README.md | 222 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 AGENTS.md create mode 100644 CHANGELOG.md diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3798a2a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# Repository Agent Instructions + +## Layout + +- `crates/sqlclean`: the Rust engine. It holds the built-in Rules, the formatter, and the statement + facts that custom Rules read. Other tools, such as SQLBuild, depend on this crate directly. +- `crates/sqlclean-cli`: the `sqlclean` executable. It owns configuration, file discovery, the + result cache, fixing, reporting, and hosting Python custom Rules. +- `src/sqlclean`: the Python package. It owns the `@rule` authoring API, the custom Rule host that the + executable starts, and the `sqlclean.testing` harness. +- `packages/sqlclean-cli`: the maturin wheel that ships the executable. + +## Conventions + +- Built-in Rule codes are `SQC###`. Custom Rule codes are `XSQC`, optional capital letters, and + three digits. `SQC999` is the parse-error finding. +- Offsets in facts, findings, and edits count Unicode characters, not bytes. +- Structure follows fensu: `uv run fensu check` must report no faults for both targets. Record any + exception in `fensu.toml` with a reason. +- Commits and pull request titles follow Conventional Commits. + +## Testing + +- Rebuild the executable in the environment after Rust changes: `make develop-native`. +- Run focused Python tests with xdist: `uv run pytest -q -n auto`. +- Run focused Rust tests by package: `cargo test -p `. +- `make check` formats and lints everything; `make verify` is the full CI gate. + +## Public Repository Hygiene + +- Treat every tracked file, fixture, example, commit, branch, pull request, and release as public. +- Use neutral examples and run the public-repository hygiene scanner before the first push. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..825c32f --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1 @@ +# Changelog diff --git a/README.md b/README.md index d0f6478..d25b597 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,223 @@ # SQLClean -Fast, opinionated SQL linting and deterministic formatting. +Fast, opinionated SQL linting and deterministic formatting, with custom Rules in Python. -SQLClean checks and formats `.sql` files with one consistent style, and supports custom lint Rules written in Python. It is under development; this repository will hold its source. +SQLClean checks `.sql` files against built-in Rules, applies their fixes and formats every file in +one canonical layout. The engine and the `sqlclean` command are written in Rust. Custom Rules +are plain Python functions that read structural facts about each statement. + +## Install + +```bash +uv tool install sqlclean # or: pip install sqlclean +sqlclean --version +``` + +The `sqlclean` package installs the native `sqlclean` executable. Custom Rules run in the same +Python environment as that executable. + +## Commands + +```bash +sqlclean check [PATHS]... # report findings; exit 1 when there are any +sqlclean fix [PATHS]... # apply safe fixes until stable, then format +sqlclean format [PATHS]... # rewrite files in the canonical layout +sqlclean rules [CODE] # list the Rules, or explain one +``` + +- With no paths, a command reads every matching file under the configuration root. +- `check` and `fix` accept `--select` and `--ignore`, which take codes or prefixes such as + `SQC001` or `SQC`, and `--no-cache`. +- `check --output-format` takes `text`, `json` or `sarif`. Use SARIF for code-scanning uploads. +- `fix --unsafe-fixes` also applies fixes that can change query results. `fix --diff` and + `format --diff` print a unified diff instead of writing. +- `format --check` fails when a file needs formatting, without writing it. +- Exit status is 0 when clean, 1 when there are findings or files to change, and 2 on errors. + +Each statement is formatted on its own. Statements are separated by one blank line, and every +file ends with a newline. Formatting is idempotent: a formatted file formats to itself. + +## Configuration + +SQLClean reads the nearest `sqlclean.toml` at or above the working directory, or the file passed +with `--config`. Paths in it are relative to the file's directory. + +```toml +dialect = "duckdb" # default "generic" +include = ["**/*.sql"] +exclude = ["vendor/**"] # files ignored by .gitignore are skipped too + +[[dialects]] # a different dialect for some paths +paths = ["warehouse/**"] +dialect = "snowflake" + +[format] +line_width = 100 + +[rules] +select = ["SQC"] # omit to run the Rules enabled by default +ignore = ["SQC027"] +custom = ["sql_rules"] # Python files or directories that define custom Rules + +[rules.thresholds] +max_literal_length = 100 # SQC044 +max_ranking_order_by = 5 # SQC043 + +[rules.rule_options.XSQCNAME001] +prefixes = ["tmp_", "scratch_"] + +[[rules.rule_exceptions]] # one reviewed finding that is allowed +rule = "SQC038" +path = "models/calendar.sql" +relation = "dates" # optional: the joined relation the CROSS JOIN reads +reason = "Every pair of dates is needed." + +[[rules.rule_ignores]] # Rules that do not apply to some paths +rules = ["SQC034"] +paths = ["scratch/**"] +reason = "Exploratory queries." +``` + +An exception that no longer matches a finding is reported as stale, so exceptions are removed +when the SQL they cover changes. A parse error is reported as `SQC999` and cannot be ignored. + +Results are cached in `.sqlclean/cache/`, keyed by file content, dialect, selection, options and +the custom Rule sources. The directory has its own `.gitignore`. + +## Rules + +Built-in Rules have `SQC###` codes. Run `sqlclean rules` for the full list, or +`sqlclean rules SQC001` for one Rule. A finding's fix is applied by `sqlclean fix`. `SQC001` turns +`= NULL` into `IS NULL`, which changes which rows match, so it needs `--unsafe-fixes`. + +## Custom Rules + +A custom Rule is a function decorated with `@rule`. Its first keyword parameter names its +subject: + +- `statement: Statement` runs once per statement. +- `file: SqlFile` runs once per file. +- `repository: Repository` runs once over every checked file. + +Its second parameter receives a `RuleContext`. Codes are `XSQC`, optional capital letters, and +three digits, such as `XSQCNAME001`. + +```python +from sqlclean import FixSafety, RuleContext, RuleOption, Statement, rule + +PREFIXES = RuleOption.string_list( + name="prefixes", default=("tmp_",), description="Table prefixes that must not be read." +) + + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + options=(PREFIXES,), + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list: + prefixes = ctx.option(PREFIXES) + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith(prefixes) + ] + + +@rule( + code="XSQCSTYLE001", + message="COUNT(1) should be COUNT(*)", + remediation="Write COUNT(*).", + fixable=FixSafety.SAFE, + enabled_by_default=True, +) +def count_one(*, statement: Statement, ctx: RuleContext) -> list: + counts = {f.end + 1 for f in statement.functions if f.name.upper() == "COUNT"} + return [ + ctx.finding(subject=statement, node=literal, fix=[ctx.replace(node=literal, text="*")]) + for literal in statement.literals + if literal.text == "1" and literal.start in counts + ] +``` + +- A `Statement` exposes `kind` and `text`, plus located `ctes`, `tables`, `joins`, `stars`, + `functions`, `columns` and `literals`. Each node has `start` and `end` character offsets in + the file, `line`, `column` and its `text`. +- `statement.polyglot_ast()` returns the full Polyglot syntax tree. It needs + `pip install "sqlclean[ast]"`. +- Fixes are lists of edits made with `ctx.replace`, `ctx.insert_before`, `ctx.insert_after` and + `ctx.delete`. A fixing Rule declares `fixable` as `FixSafety.SAFE`, or as `FixSafety.UNSAFE` + when the fix can change results. +- Options are declared with `RuleOption.boolean`, `integer`, `string` or `string_list`, set under + `[rules.rule_options.]`, and validated before any Rule runs. +- Files named `test_*.py` and `conftest.py` under custom Rule paths are not loaded as Rules. + +### Testing custom Rules + +```python +from sqlclean.testing import RuleCase, evaluate_rule + +from sql_rules.naming import temporary_table + + +def test_temporary_tables_are_reported() -> None: + result = evaluate_rule( + rule=temporary_table, + test_case=RuleCase( + description="reads tmp_orders", + source="SELECT * FROM tmp_orders", + dialect="duckdb", + expected_finding_count=1, + ), + ) + assert result.fixed_source == "SELECT * FROM tmp_orders" +``` + +`evaluate_rule` parses the SQL with the same native engine as `sqlclean check`. It fails when the +finding count differs, and when the Rule's findings change with the order of `files`. + +## Editors, CI and pre-commit + +Run `sqlclean check` in CI. `--output-format sarif` produces a report for code-scanning tools, +and `--output-format json` produces one for other integrations. Editors can run +`sqlclean format` on save. + +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/sqlstack-io/sqlclean + rev: v0.0.1 + hooks: + - id: sqlclean-fix + - id: sqlclean-format +``` + +`sqlclean-check` is also available for hooks that must not change files. + +## Using the engine from Rust + +`crates/sqlclean` is the engine without the command. Tools that template or compile SQL can call +it directly and keep their own configuration and Rule codes: + +- `sql_lint::main::lint::lint` lints one SQL text. `sql_lint::main::engine::lint_json` and + `batch_engine::lint_batch_json` do the same through JSON requests. +- `sql_lint::main::formatter::format_json` formats SQL. +- `sql_lint::main::catalogue::catalogue` and `default_rules::default_rules` describe the Rules. +- `sql_facts::main::file_facts::file_facts` returns the statement facts custom Rules read. + +Requests can carry external identifiers and declared relation keys, so a host tool can tell the +Rules about names it resolves itself. + +## Development + +```bash +uv sync --all-groups # builds the native executable into .venv +make check # format, lint, type-check, fensu and clippy +make test # Rust and Python tests +``` + +## License + +Apache-2.0 From 6fdc62fa3aea7aa9bf7a116954bfedd1cb72364b Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 13:42:09 +0000 Subject: [PATCH 07/29] fix: satisfy clippy nonminimal_bool on the CI toolchain Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- crates/sqlclean/src/sql_lint/_helpers/additional.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/sqlclean/src/sql_lint/_helpers/additional.rs b/crates/sqlclean/src/sql_lint/_helpers/additional.rs index e562b59..301f421 100644 --- a/crates/sqlclean/src/sql_lint/_helpers/additional.rs +++ b/crates/sqlclean/src/sql_lint/_helpers/additional.rs @@ -797,9 +797,9 @@ fn unqualified_reference( && !is_generated_identifier(&tokens[index]) && !is_context_value(&tokens[index]) && !external_identifiers.contains(&tokens[index].text.to_ascii_lowercase()) - && !projection_aliases + && projection_aliases .get(&tokens[index].text.to_ascii_lowercase()) - .is_some_and(|alias_index| *alias_index < index) + .is_none_or(|alias_index| *alias_index >= index) && previous.is_none_or(|candidate| { !matches!( tokens[candidate].token_type, From d598a7c2142c11cbb33faaef40eefa1c3c1c3655 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 17:50:38 +0000 Subject: [PATCH 08/29] docs: add the logo and a check and fix demo to the README Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/demos/check-fix.gif | Bin 0 -> 213152 bytes .github/sqlclean-logo.png | Bin 0 -> 536016 bytes README.md | 16 ++++++- docs/recordings/.gitignore | 2 + docs/recordings/README.md | 31 +++++++++++++ docs/recordings/prepare.sh | 63 +++++++++++++++++++++++++++ docs/recordings/record.sh | 14 ++++++ docs/recordings/tapes/check-fix.tape | 27 ++++++++++++ docs/recordings/tapes/common.tape | 13 ++++++ 9 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 .github/demos/check-fix.gif create mode 100644 .github/sqlclean-logo.png create mode 100644 docs/recordings/.gitignore create mode 100644 docs/recordings/README.md create mode 100755 docs/recordings/prepare.sh create mode 100755 docs/recordings/record.sh create mode 100644 docs/recordings/tapes/check-fix.tape create mode 100644 docs/recordings/tapes/common.tape diff --git a/.github/demos/check-fix.gif b/.github/demos/check-fix.gif new file mode 100644 index 0000000000000000000000000000000000000000..7c22ab415822f37be264b422c9086239041cc7cf GIT binary patch literal 213152 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 literal 0 HcmV?d00001 diff --git a/.github/sqlclean-logo.png b/.github/sqlclean-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..24592d06e4a94efc96fcef42cb750fc2dfa0324b GIT binary patch literal 536016 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

mM*srnH9#;se% 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�`s^$ma_ztVhe)-qTbz3g=;K&+b}3V zrn?bGXK?8xI1&z0&G|%)d(eiwC}U4y$M)9v`ZkD|9-EVrF10T_6y~63fV^xzIkOMn zAR{W@rZm~@zQ1L_Y8G3_^g-gAWZeyFuwGsCp4mVw#~>n%2qJB$q|UKEl!@@SJl)A9 z{qlbq^PbP!9i4dh%Bw9~Iu=N&eztP2Rp#@)I$tSDat$N!eZHMNm^S$BlfQf92l$Ppa zFd;zu8m#d~C}$zM?I<9H#c1tHTE=;uhth6s=n?6jLQlJ`Lx$w_iLPf~d33{84)5Or zclYLV+xJ{z*}A2AXHkWOYqHFxwH6jGJWKLmk9s1=IArCw!UR>JbQsUv_D?$!e-n-B z`riL5S4PaZ4ccV&Xj8TJZ-k`aN$+|ycIQv@-G5n#5b=F=}_t z&V(zNdrtea-GnS7jiCwPzE00r;(ipITLvQekpCH?Qn0qG_JHiXc7TL=)=N407Foj^=f`X%KH4J?pDzias%8`iO=^Xj$p1-R-nnU+rFpoGC&>r%3i$&zqieE!>^Z@ zoFD49)Lm`Wr2AtF9X&I}Zq=%`AENbC1oyOzE&)R8YYPQy$( z8-lbXMuGj1@NTK1z=JDgF z;an1(r5vLa!w>5$OGowvi2;ySOuU)c9DF30vcalDq`(=jTaAL|8g^)KE*)2cL1(0P zzpU7}OTwAsoQ6?n5=cel;{5;1lf6OZvqDuFRBcJZ5p>XVQQHg-fl?FQ>Fx{($=Rg! zrlyZ}@?%%6U=2t?dZh{Qey%6E7HMBDD~et{jDSNpxTaL^PY@u9;2frH)90!x8k(yQ zLUyq_N)(f`0~_)pLnjw}=u!}pT5rJt+UxPVdJS%$gO_mg+s6_S$;HUc7)#om@=rhb zRsFeNbJ21H3d1bW>4SnS6q_nHeP7ECGxt4YKe_4!7xDL{A@3tX-nVphw|I=Zd4LHt z;@>y8&f)Gco43N>71=Wwjd))uO#LlPpMbRu?9%dw+nZi|EF$Cb@7g@xwLBSFX?8Fn zGU&ojnMeww7BOKG_+G+^(s#|`&Qco|q&$aBPH#Y%1SG~=DT(0=Oynm766RFXO!}vv zcgDw$kU{;}&u3z_Fz)@b_LA=fxlQLJg#c-_V8Z8-KO>YtD=Uah>4YKH1tsZf@$60@ z2?%iUVziE-GoEZ7swhD{!oo~o;m+-|32&y6=|T+GXW|Gz0dD|~o_S9or*F{Ms{sUFFL{PDL<9#QMX zTen>Lm+C)10u9L7INAb)#4S!(#YG1-*7YWB^YTE9y&pC$_a7Kxwf35B5p0c>2@_qEGY5fNyb0+yWJcHaKb73M0D+=cl1`42j z=ad<9(g_MDbGD)AqAWdPCWK~;L~T1=S*0d>1*OWnMde8^CtTasuwfGC*=d_q+8!7O zYQXF*O-LkX($ng3pYUObgTqm48+SyJn?zGx&c?9#`bAp7Xx7=pft=m2fMox+LVHNQK&EzmkJz-W-^HU z3B`|H$EvEg4Afjn9J``ZhiGqoyZhxn3<@iqw`|$6dQ*lhhGaaUH5)7x0{-1bDF4t0 z*jORV6#&x1)kdD-<<~sD$xKijoEr-4vEZ>Z!pe*AO)l!kZ7KEkQX74h$YRmH2d}pW z_YYMK1y!qILHTBQ@?l=+KC2Z(-o`~S=w(4_08Hp2uG>vX-Bf(ugv?@a^2s-EsmbK< zxKsoiAvMwQMdbF`Doln+^K|QB{CxO$B5*hvga)&~*j8y~cg`t-cZe8NYv5f98TvY8 z7zFLfg`XS>N05G7e*VGra)%X1BJwW-c_uKyFeQj!$S{O!WMDB2M=z@<2V+NT@MPc+ z|B@Lpfo~NzoK9fbFFX10SGMgg$-U`6{@g$D#hN`YfK0a}$U-Ar|0Egsf)7*RHo@G-231W{D z(}oi}AFD$xKO6VVE#Ek+Xaze=Apveo>L_*LU`7NfDGxHdBaSyUxL>il^Q2h4(K^$K z#7VfJD~Kex(TE+b{>@2J=OlLKB;MK+)t}!InVH$_$8k&Grf;Qn5^DpRjt^l>^4xzh zGHf{Ek5>dnM=qD1klXmoGR{QwKF3^)Tw`5aaXQ6B2==c$frEBbPct^!4jEgh0tS4s zZf-#5knU$xIqv!IEA$5R-v3(ij;FU)WWSLYgfK`L)J&DE^HFK}Pn*h}8buAf6RQ;2 zba>;3qc4QIexHddA=_jbpD8X_mU&virJ#n=5^C&%wcvasiE3)XG{@syf^HFO;2Lox z0_b6Bx86~@T-_>yeJ4%5<|x$fR2HMzAXMu_M_B|#96Ueyru#e!m>RYM-EV?5-bkIn zJc@fbb?_91wXl)mSVzTO;ZH=D;mAwI0fz<@fo7jeD-TsRSK)HkB z3n4>CM=;KEtA~F67!EE-c0-+lLX3DGUV7vu@{;l>z_;H71%rn<#RNzKFnhaTl!h6! z0X}CU%vvXyNI`BPvd7jXVNEtK{Ah0Z@rY$2{9x%lTecKWo>i1}FBPD(l$hVQ8ZAtG zTW3*m)8g^3O(`C4u+IUpdHcYr_IvjL&LIMn92FO71J6v72IJr;xt?qJ*0SR6o64Ix zyVW^BfK`V_zx(ikV_MV>I=w@&%UbxH+$KLK%tH|(5PEshmj`$Yqn*kkKp3QC#uUe# zO*N8mhw<#P(iE!T4`j+5cdQ(dv)LU|!5 z^^zEZOBWGH!*L1za)OO`L~0D=MCe#jo&pvkPzyY9(Ws*-8Hl_-MaCkD$_t0S!$(&S z6n}|P_e2^;$PNQpN`ip#pcst_ZJPxXkIb?;hPFm^3Hi$9w)EB(FVBo29+@;p;FuZ* zWRo%2p)Qw~|Eaq=`4>R%-^ruAKApb#YuW^sk|}hb7K)zS=BaYywMb{AP|zL0pw#rj z=S)EXtFnQaVn_4dD^s1Gdu4ro*N3mPdoOip?@6Xm^P9rI|6j+O4F5!r>Nl!MO}>Ht zb55N3-O+%ix>^L&+ z(Px)-cAYhC~6XrCM=BgQVCKNIlU|X&9F!*T@ zGzbD7Ha&Rl1U07mb9D7+<&(bgO&7T7D;y-J+o$VHRC%k;0!jykh|~}nc>Ba~*-M}} z!MKNr1s@VA(949NdqE>A>flPiO>)q$yA#f)^`R(Gl2OE{DvKuo_^pfRrhsm9FR;na zhqK5xEBjqX(%`(p(NAZq+7?wco!$Mf-TN7fT+)}u9W+FI$mamFAjJ^~vx0+^%sO{F zE`EM~rxP@JM_4|t7UD<-sAOPt8WMWQQ@jcpdJv)}&(S-(;{4yC z2(Gor&@zVG)#w| zl;8%c6N|DCv>3xp@B4yJr|m%cOVR?v{p+=V%Bef1hf5V4euDtTH8Q+=j$woku6KY~ zQ^k0@%(~IQgum~h%H&I2WVwX(GShP}_!lvfiM`Qtr&ay7b>yby$e#~3Js7UMnJ1?l zr_zBmi?>Y)H&l9`M2)EFcbmqsla8u)_k8d1?)w!E=O4^ku{GMe$kc}mR3zP-Q1)Ka z*EgrZ2TpMDEK=hIaOKuB6l9m=MnguF?`cGI_Eqb~EI%x<{B-rU$L3wp(Yg;-qI-E} z^Y<#uIQj-$;N}x-Re8X3X2VBiq3`W|!)6UrRt%IkfWp~MrtW8DtYlI&1nN2OxhsGH z&EvcQF%APR96roEmm||q(JkoW=Zhud~kb@pCr(kHz!TPs)JB589`o<+gHnx zVG9Xqbx*?F!ELaA1Fd(NDA*ohR)|jVM-VTd311{Iu?1B8jhtYydNOI!0R%59(+L~H zUN}gfVWiRQNgw{A)?>gI4_3 z9P~5boxnt6Nd-Q_IupdDNinqZ&PGq7=su8#t)uC!n){I765B6zL@$bdM+BYpLmx(_ zBUc}_v=_d4{oUcwW0yvcy+&2Y*uch3#(F^}p03$T1ewJc*Ra>2fobc~*EimOc>3W% z$oSV)-Js~)3f7d=&iCsvH75H8lIj29YwZhHnCY9WSqqou@jSDs_ZlMvIGSZ1cOWcy zDhMWzT_X&NCmgbULU?pO)3Nh+Vo7cFor``V86lXv-NG`m#~Un?B!ELxs*Z~F<(i93E4 zAd29MM3H(P;!x>7`Q`+K^!@S1Zuh6Vor``n*((OdAqA2y4X10q#2A7!9V<#uJ^Gd~ z3guwEBaCg?6^sMjwGV)^^Vn>!IX|I%2Z|e%1(tV(vfB*H1O#{KPMU#`d z9&CC3m-QyQ!>6aB?Ttq0OB6)oR4^y@D_n7-rUa2gJB4>9qEKuuKL?4KQYXG7h9dF> zG2YbgFS-2MHf2@cuFepb!lU~QYigF`)3X*gseH9RpjU3&cXra- zZz{b-X(M8TloU)6~x`2y6p;;PE^= zXtR0!*0qKOV18`Rs7HquXDzljZiAGD&47oDoL8BF2@`p zcbXrOCgg;1tXxr&G*<+6=+30p;fj8Z+J9ukAE;FE1wh0E&bio(QGycX#z12d>S4`1 zF_6eKVhCYIVG=yUUe^IJLx^J;#Dm+%XIUkM*F^G#ilR z(Y+y!@)!aN*PgC#>)Hyn>CQTk`rbgF+nID}c=wf^d*(fT`Lbchg|AU*1{I6R#Ws=d zLYTn6<%h!``X%w%g-N?0wsEN~<6?2oTqT2klAyVY;L)iELDKX9 z^Om^Xa?ar@Y@Y8)&Dn1EJ-aH>23j0!)U$Slo)r~tV7QK3d6;_@vZ%!w7+RB}18~FS zgFm{2FoT7V-(W_VG0(cS-MO49D=n|;pEuSLGIA?qwA6X5v`hW2i#ocl&FHR~%fZF{WrZWB?UvPTDqpy8AvOKxX@x#Dr8w1< zi~7y=?cykS9OReXjHp6UUG?dRNMDnt%yb({CczbE!Yc{0E?zSWVK=!>KxpLa)_I|j zEE#}J@SEU|6zl7wXwe>-%1kkj-tB6Qb|!D4*CUf0AWtJ5rrM*h*X7|DFK|w)n<|H< zNd9l#3b;-!AOYdjMS77W@q(0i!582u%x^LwA!#}2i8mq?TZIg?6P)=gM8ppT`+#vu zc0xyxs)C6Dr8*_yh!1KkR2x&Q0ixfzTeWjU3FKX3 z`{7#Xe~Vn?5n=kxZjJQ<_G;j@UOtSzICc*|Ryz;%xuns)q@R43gfj6Gcmt8cNycz= zwP`JXebTrQjFg3%4bEC+d*J?4tMWDm;cCu=v5PFF!(fe5+}5z_C6Ez_EP#hbp$c&X zYznss+nV|=@ZaAmwFjJarQozo1CDvmW5hJ`TU}A8>{*Yq74l-d;ug0~7!VMb1IjHf zk3dQo))0n8D71fpNXo)b6CYA80k}mC`T9VG;6k|~6)N}qn;>qHJ8BumJS`EPr3xIq z8PH<-p#Hhb^x&+DWQk}d90}7zFIi8}+&g&oo1u*)jt5rQoiQ;nD=$0|BDqkK?$X*w&PKR6q>+|pqW8d4 ztS^R`Pt#XRkCtp5EZ3gt|In9cFcv*pz?vxWbN$2TUB&vCilO}1WsA2iIG>QX=DT;x z8C2_Zx|tFR6_Je7cLrJ2L|}SsUYvRrF&+tv38y-pfx#}75K|g(s!FF{d~k3jWZe%3 zt84!`yOQ8BEgUiHzr@uCCX$8o26fO9FEi;RYd+{_g8Ol40$_LgP^#gqo0y`=TWQAD z|HFRbF7 z-u81xAT6_w9hWCAz{Z7BnMX1O)JJY{%fUM3TJu+pdpfLjnaO+JzV!g`g0g7;oYG7S~|* z_udYpN!>ijF8yjn4~f87<`CKcyw2cOvjU+OOy)OkOR5Z}T&7t*Rc)NgC{^9ywV336*q< z@#rs2fLn!3aZ#6f2Y|(Hi~_;6H5@||25A6>kAR$K<|Dq3{43s(t*O`im=s7uubQ*v zZWh0ktR<2La*~F&gDQ?{JdZsCHMatyQ3MJ&BxG%Nva~qDkq@WfQUdK za5poECMI*C;&>8Xth=)9!?f zU@TD|J0_340VBkwjt3@r4vq39f>LWLhuMPkoC{r-MdOiIBr? zkzR|gon4h=K{)%nw}g-rK0{`Y)|#8%g4UJT05D~kzQPwz*b>k*M1LhEtkzmk(B1PX z`F*}Ma3bC07)q9*hQtv(1Hj=|Dn(+{lSl%o%&AQh#Kpl=8lEzIRH4~d(qdo(@`eEx z@4?Vu&fs30BQSr~1E7RX4!B8FDrV3XWGEAB{=o!t^^bE&(`}(D;DoAf(9HHBn!pgF; zZ&rSPjv_G?P(uyjSfIQ`p$Fs8e@~tiS!}L&)wZ~&?TL=|M5noC05%v)WO0veJ;y+b z;!(Rl175#=`E<{dvC5GCpYft=?fbGRDiW@jAAbX!Dbk3impo&uVT%0% zTM!PD@NI_DQ0jRRc2MTPZhbgUCIPN)M69Bb=bbgob?yiGjC2>$z6>`lQe<2O8)QJp zPDo-Cgb(0P=8E;GPq9^XuX|26^SE3Q4yubK|N0%HY-z?H}E49@>|6kp(yW z%Oy1cS+P@OB~JTiJI*O9!59ioU`{B`EDD@i!-e}@0A!vD)6*)!pZ-hEx>~g78Zmvzb!M_lq$(VDhKzhZWCU!H9&v2OIB~3tuCK zCpbbAsFyV)yzxFsGNe30N`w_JF-fEI(QX~qlO1z>c{9eZo33f^;JfeFKIw;PVDzu8 zGkOQFoo>GTI1pkZn?P8a0Z#|anwS7!w1l7x4EaczpECBm#VoHr9w zc^reeG%R`w3E1Cjn5y-?b1|D0gC2oqhvGm9A&^pVN7@mFs?wr1xMDn zJiP3V%6A=Yxj;+9cMITV=9praFavNua&SZKT# z=zhZSsz=9v^5)1JjbU;Kla#e_GZ5;Q)BCieIlC51egGs&p)- zk0gVoWH=fqh#EEzUxkE1XY$!3Y!Ma=jMPkeCl0K41fCf!HobFKUMRnC!N~`rSkmCn zUE`0dKcBDueD(G?O0KVwHGjPF_7Q&$RbQCtJ?OXhB!@_5dCrG`h`|Z4-#*0=7A#Tm zwPsiW30Vn?9UgV2*yaW$VB3iT&FQnd+oSfhhiKJ>xFzlxc~kY|4HYqQ<7dSP?rh0p zdFCY0~l?G{xw zGixJZXnVqSzZio=b|@m=%RL)TZd;Lg!8V-B{=I#;EOc#>>dGF~yDN{i>k5bR$Pnf8 z=AqwyUa>NOX>9x_%T*Lc)J2$4{iyZGK1vD7T6A*)0wxwm+15kH>O<60tJUJq?c1%w z<9T*Bv;z%x6s5P&9t^jM`V`Mh(+0tg@G~(SPwxXVCfaT~!?iXT;)f=~@yf6FeB^He z|DkjwM~S{_9zw;wY(wzW_|Qw#ZupGY!;!te&x$aoQKc@7!G3(;5B}S`EL6L%v}L^R zU|nx2nW>lx^EBqtha+hmq@eq_tDi}K8riGKe zr8;{F>79L}#2bK~1rp6i&hrwq&`68%1#a~~Gd001Mo=g}VX+~U(qKBSJz>oTQJ{@u zIB5lZ!?G_jn4X(+y7~&v%;e=Rm1yxHjEo4Gn_B>@$ybSz1mB2(^7XA zM77%p@Fb28&3hETC4Th}_w@h%-)aBHn=*mHk1ko-Fp^}3$9q`<^K8o_2 zq(18imei#+2Y}j{pp#?9R_`w1;hHf!T_64;!EhPe3M6Te=IttA5Fm)H%f5!pjHAG0 z4Z3$Q*T6HE8sQ_u)Yl^x9O~&n7EM>@uTgF&VYjECZns{^hCx(Br7L9)gmm;u{9jNL zj=zup{qOL-d|r(!K@F3Y(23L7F|C~Rh19TuMmdqfx7Q=;(f5t3Gb}lz2Ez;^Obi(g zTS_-S+8vYFF?qDDddaTY62BiXJ^m-5|G|IN%D+C{vfTCQzqSPJan?lU(HfRjsyFmu z9-$!pQ1zwsde~}lqPFs^cjbihnFi}5sp&=&DQt?T$RN}Q%esB|=%gBOm^AMTqo1@L zqxel%pn%YF1>zaSdJKg->CN_#;I@j=!Gok9!gBoYZ{s?Dt1NI|CdD>EF(E{_x8=7?Wx$~V02ZXjYlVNnoaD_W zI!T`^DwpUC7J(BxSc0e(6?X*4ugH_hB(k=kU;PBjRoAun0qW`%C(h^-WJmE~P{`~c z*+ce*!Ua|_2Zx|RP-Af+35@hTACZOaZr|9xYIT0~5aJ?s)t`K;KLHTb+y7xw|CW}# z=;YekzqYbXzeE2Z>Q=87#t;-Vhf}EH_h;v}Za_%65z2`^x(lFC!ra7QEyqrQaxTdG z{N?WXHlR!*?G3*w$Yi#m1QJW2{7_81?c0Kaf_hguo_*ouYBH4)6}(pmXRQy%;ZT z76zf;6{?IkXhjOtka_?woTR&?d3{MU6f!>bzoDP)e;*RrNsvMmO5igA1e8fw!FkBk zGnR4{YhmK<1QuF+{LAciP3|-WgYTVt+1PV_2Fo)*Vs%ROvg6H=Ka_Cv{AnMn$fHOH zEEM2V-Ejj9{TWrXrN1LGon zV>gSp4Ze4Vd9V7Ns_I=-Qg0MUO{-4scszd<^usxB3{2I+W(sR$Y?erTdakF{4?noZ zRjs{zZoC_AeW-w!C#z+X$DbnJqTaA|OL6S?EA|HvR4%1Og4lX7fkDi{2`v1`Jqbw3 zgPQP2vVk=(4_?l7D|FM6hH^qQ>W~k<)uU)TN1^uZtuMzCD}!^GWHO&=%vnaVnHWob zp*y8oS)E=jg$V3h!NEE_7>o42cXwBVK=&sdh8w=Dx69fIkBh-2HO595R}yc3!$=mxqJny*_7jVToIofZR3i+Z~N zZo@4F$R-$Ym!V*^7vLtuv|9_W6-sfr!3A4bUm-#i5@V__OxIw*USVwRh6)87rjTSj ztpJGy&cK6KE5F}$c=)$g^bl{nOz}m^6F4}JL$a?VJcq#i64l8$N}1gK2-KH%%d-5Rz&sN)o#A$lB-c&8`W>T@s%a>MWY z{dDstw!U>SkzD+haZz@mHm~qvdE$eP-9w1D$H#A*pWh<>R%(heW0G9Ot}}oQKmjlU zyV=0D;HVUl@o=!vfh%mnk(EU4coOxb#O8|o0)C#o$vEOrWD)P}zC2WPLc@FL2cu-1 z8w}@o#&VEjp4v|=_I2C3KNB@|4w6C>^HTBOr4nY&~z!E) z!U3^jLAr6oj3Zr-LlGrM$WVRDfzV7aI!#2yhKxsyuRLCz?drua-FAVWFJVEe*X$$R z7UU5{Hw9J)vQp&WasojDeAR{+$J25zpS3S3x%TQ)XW6h;1MfI#;Ma^C9)CHzqGoml zLbPBdv`0VMQk$_}K6820_g-G6`uk-p4!DkAF7Ro{6ah|=g2*#udOw^~;1J;w6tY|{ zMzy(<4|9H|q-*3tm!`yW^jXO0A0d?(^%Hx#u4LzJw9{=CK#XAuJ?nAvKw%OVe#k*# zh77TT+xP2kT+AvByqNd`Dgd%RhY^fU9Niu~JoKd7=}9*RVz-hvZt9sI9+_o7YodcE zhcwCZF%uFKAkt9r{76ns>a$MD2p-UH z>NEMly*9zGzeO{u4Fxw$GV5%J5g0)3Fmdm-0+1%0Td-0>&rQl(0#{~tNVo-{l6AEG z)Aqyq$Z>(I2Y6ecH^T9RIpLlw{CsJ701ras!*af4IfWg@32OS`my%qS2Ax6w_v`hg z@rTjmlh(eeev)6Q&cD-VAUXz$;^~uF3cy_m463bg*@2dYbS-AKY;1-!*fQ3^cxt%g zSd)0sD@UsRcu1~oD8lLT)^QL%-Q?9#eH$S`mE zb=EKEvdxPm1)BS3cQ>xt-5IO-99uaQRB=mHF|Km|d@Gnbf8oOECydLzy!3s(I}lGm z9km{CL?$0@bVg5ztCx443;%0}AWg`{ z{JTaV0_vjPn<`|}AtnHyiCt!uMX?jt=Ra`lhta16w0GCQiLQaK$V*dM`L%0GC8zAo zCb_u$B#S6w@>pKzqU4xchaJUq9F_w6<;I{~2|4xw(tDg>7LB71roQKTTm51ueKbD! z91y~d3qNIYjJ63>2z{2PXJZ8958QoV#z*6p9N>ZiVv*YvA7y>LQbI8_F@dQBM~$S# zgQxf;pc~7#P6T?8i*qh|5*3tgUPMckn-t)_%qXB8mVEZ5uU}sF9^72Y)Zm9UhsGKL=bY9h3_hDe;dpbF*iv|0 zJ>pNQH>Gi$hwrq>(6yex0vFzp<)rT&WzQQQ@z;#}dQF4CZ8yo*#mEqo>aeIY*ZeY( zdLgpm_ktWn;b&u@kj#Vd4rB40DZ_598_?Vo##NNE5JU&$Kq73&3`eYv@O1YjQ+CVK z#@6*NNF~g}2$1~fOtNd}d+!PnrnSIRq1325+0_kAm#cy@I>n#Yz=~J+LBDXzt9wPA zyBBAKITV#gqEE*~XR@DV?5&(@@DJzLH?uvSz07udwCC)pD;5)vTX9(^wW(KflHvO^ zTwcSpfHxT{KO+>PGaPe{27p>IlJ(Q|8kp%``Ro>_D~Wnl=XK71bUeTfIE6BvUF+N5aTOo#I>UpK#7&sA51D1{J)Cmrq}^#pc6hB*fM zHJiV0%0>!gkL8=8QGcXfw9hd;z75*G<@n2QEAP#&x`)z-^VKy?m71?RexCdlOqKIj z%K!4R2M2?v?}#L_im8$q0-Fc(t*w+VN=AYY?WZg%*RY1^7@mHlxa340gk*W&U_!@s z1q_a38HntKjHZT+_IQl7*?GM3QS4rvbN`_!_a860+CLPGX99HIF@5Z(tkVe`io{9p zzrct0FTPniwri|<^iB1^qUy0RL}omOKy>?;Y>pVb9{XW5-$Esfj;Rc$0hLhx6XaMH zn?{O-3{-#eC8;B8jL9Bp7C;c`PSPdrcI(zhN(~u?RQP6v;Dsm|C?_Fkt5gEirNQ%a zu^nFg*zt#O&}3&s9hDYy_@2@wz$@vO`zd61#QLjQrRf;-`!V3+!D9tQFv>(X3*SN* zctN6c49d{Q7Z#=xDK@GyQwWfOT9Rhi*&A`BCCsCnc4U>7tW|2^BK^N#a9DTZI@F&4 zYS2&zns(5&nV=4w&0p2VKLqTwHs&>xmQ+ib49;hpUjq zyL_(bPE;J{MD>70^=DrX-N9-#9@ZKhV1*qI0;4XJqhJraGiHCr671q|eochoilADr z{vf6za!u7=%@nKFrg?EO4nVS-{FiT(_qLl9m#D9};8VyK!Q|TLg|LJ1kvsVPJ+RMc z4i>7_g}V-7A)U8taNe%YoLzb%_D{9^UqD}bXiUN3SfBii5~W7C5%#)vqEC`y4b?6M z8vNEXCprV&FmuY8dG+0$MTy-AOPo!7-$$3+C&u3!h-Td+!J04Z z=BtG`!FgoC^NNB7Ym5V{aX%$F3UFK-p@S1?x8nqVGV%Mr1~#DYrKd`-HEV7tAyp1W zX&=zNmF{+ffVJ*tiQw8`Lt77A4+4`nGjTV&6W0lxsefG=JbPfv=sr{v`M?UjL5H!UjfmYg`zZcBnzlohK@ z7(y5n)BVXT!vHw&?T_=G2cfChvJ!D+$fLesP6o zgG367vnQI`VZgyuu6v?6axOqYOVSARQNR&a1D6hkKkTa@R)95RvC`y&LG_41-<`s7 zSgxAq?P{ABbg4sNlO^Ds%d)~ZvwzGS)g@8B1&wHkaaF-8!o)^Pc*1`HcdG~Ti;i@n z6fRgyz7f-~;8nb|N)u9-KC~*+-Jg8?$;WqRRGSap zYx>EQdvFT}MFbh3_c1Z{3PLgU5uC0HeDqOz2A6<~34>cKr6`0}v<9}cX5z0K{&}?R zMAFl5HRd4~26w$*ggR@VW9_G;YgV3}Hb^4F7hd}M#5d0VUN;%V;RmD2C|mR3B~wWv zghZ(^P$p>Jb7j)&iR=UfT3zKgAtVBGK*qd-C{`A#eC=NzJ*p&Wr{PJu@Yr1a+$5Xi zgvIbZ(Dr}f#8j*{qhf`j;Y4ztoSifH%OzfCKuL8A-+Sus^OY@oh9AeZelv`Npzh9L z&4vt)_B#LP;Cx$c&`^+|d&0HYeypN3|N1|FcP>c1S)XjZ8kQ+qbK~YC>*H zB&BWc!+Hi5lFr@)WdC%5RGMgziuemKSSf(jyZb&gx!mqrwEA4cPgwmt#xQdz^U#$Q zj=n1V4Q&jKYj$OzUTIBIc}~K)oX&?2sp6l`zu+4Rz3vCBFg;CD2shxSi2kUfOOUFd zWAp^_i2KpH@x4*TRBU;`y&Tcwgra~Che7-k%Z&<1w~NX;<;m?0%@@xX-M1XoT8@13 zAG>cbt<_h_2~FYACC_ENZ%Y7VnEBJ2tuyuw{gzw5QT#ORZ?wsonz-T~u~ek}`3oj2 z;XCl_)B0L-XCq-r$d=&Tgr=|fNhZtxJKpJvKgH>8;F}u@|M{tQlfG#1k{t?kZ4F7= zQJyi|+raTkbKRe7n@8uu@85S~=M(k8>XD`{9qn*pBfj;Hgvt&iBjRt}u2LS{xqVW4 z?Qb*e&RQSvv^l`TtbocfM7OGQF2ejq4c)E=s?0XQj<}}g=4}&)ex4W9eDy8nOtHF` zhqX`VCB50H?M68Sj7$spZ^rB+j-7e1Ne^>%vXTm}3phA24GI+!%qf(Ugsk|c(3{BE z`pu!9t25g@d*((?xBqUF{vTAP5Vg}N$cuLJbgBQX93HUV5SSwa}@~ZBBzB7rR(X*-i-{!P{=vUV-X6=oi17b}H1x(^} zs5}=Nnw!N=@8)KusdwLrzy~Vnqe1nDLGoku!7k~O-M8oe3W)B)fM8`?cs}T0g|%`@ zP(T*7InwPz8$jzF;D2inAbU?pkH%5!R4CCmgZSSELiL7DJ=dzxxf_YJCly~c30AQAgwZ3|2q$l`0RkQ416o5^`O z1rv)3Aq-6JlW`0pWA<*kb&hc7&2!j5NI1s;;CYc3K)G^#etXxFRNrr=kHAq}K zg5-x1ple{0_l3h=N*+#+V0efnhD=jc2;sB`hkK$qiT^`eMNxCo{K0;_XVfecDsl8P7pz(PFh1_GI7ZS#nEl!?)Y^%$xKVjvUCJ zy^kVu^nwG#%+ca~Cl>En*p|2PYz7l&S_l}NU?$D8f)5Jk7mt#hpKGDDcxmc>o6(6jCk@X50Y{n?*GQAOLhaq;nDA z2SMG4i9b7xDopF!5X|yfZ;RePtAZ4y?nuM&`Z`)#(h@38OWwF+9W;ar7-|6-D|6F@ zr(0(r#G@YUTm1H~#l44dRKH#obZK9}fef6|)N5SW^lY*3)Y8KtPj)RfO`bt< zHS?@z7>am698qc>`PX1ZWM*J4HG;k&4ufwtnc{f*Xxl%%g?c0^ofP0Ua?57^^OE%0koSiBQbk&R9t;+z_H zIG(l*vLy#pdmPiWPME(7;Bz2EG>)px`>$o+q2PVvrg!bC%{PYRszjWRdr)7n>*Z3* zQCJS1e?cFIye%QgELxDBD@sakj@NI*ZX@~1SWqP8nKwe)Mf_bck%IFJlLaZK;FeM# zKe05j_rC3%^W}FUD^1t$==f%*4zBALg}PTE8Z=|$*MpF6#@mA^%bKSd+?-q9TUReP zVtx@2U4h2IeR?X2zp>-M=GK}+@LPd&O)0)``tHN~r?S-I$T-!zor`HTBx`>$;o z(1;huO>ZmhgvmNC&u4-31vAd0_T2Y&H!i-pzWL~R?Kg%C_w;_Mzhj07j~u1M1eH%k zqp`l|pE@@GKkqAN>_Lv6e)|>d!Tn#5OVbhurp?4-huAZ73`M6CQRsY0g9o2^@D_te z@8PG1MbdbG*cWBn+YmSecP|sEWUIpN<*kZ}HY#>Hw_|%)7=`GWuN^2He`Tq8(vQ_p zbF0TvH*$9I(79c^c0vA`Sbol^hY2$rvJo^)`%*9-;1e7mcaIQ0-~{~pk7{vA+xDM3 zwrlbod*cW&X`ut7?-4~DD1u2&2*&ms7?~Zg!YSt8YPG6*EWdjEM&Xz`BpxxlS2=ss zIZ05M|5y|MD#s(?qiIe?5Bg!zikOy$BI``ZB2bpw%;Yxwdda)&Reon$B8=}2pWqST z435`YqQw*-?(up=e!9kScUbzcC`-NJ$c2oa+&nlGdTv6)e@+Qibd}!~%sp?OB zH7)7cAHf4tL)2e~j1L75Z?_!#`!-eBG~@QxX46a=Nw@Vv9ES-e$LT4s%%VI)a&pJ^ zCr(%2S{8h`8lt`xqOAm6z+<%5qhh#fOp++Dj;jpzJs`7@dCJ?eb6d=Q?!ZcREU9wJ z?ZX#tbAzs*iNW;!-sub@*X2|m=G=y5w@$seaj}17&1;HsL84#41|LALNKVsU(iyi5 zp8%+3F{+~TL5qU$Y=6F8-B@2wB?6oN2gWeKANYW`^iJGAZ56dxeJOO%(zAPkQey`L zmlzZ^jKs0gBjANCf3R>F&}7wq5@=!=Ok=~D)9Z0e;r%}X2daITH;~J`b1vQ-OYjHC zp*TVv1Y8d>!S?w{PI#}r=83ThX!5~X%^8`DiCx9txCNof`W-Yibq-uXFaYO(Bqe?y zrTwF>OdpCnSxSR27ZtKu-p07m`wpuk_a+EB85AY1og%TSm*V>Pc&0#r2bEFVKx4v* zw!d!!eTJ$tFV@j@3cO*CsXP`Sd=uh4tc55TUgcSX;4c1@3s#;cPfxf1HWqVb&%eM8 zd1%JF$V$R)Yv?2Zc3gb3>4MzN$iat6p<;I-%vr>836XrwQ=$h0%UCB!iVJ)oc5%T% zgP{ahu`ml*WWT6ZIJ4*2g3YJ|@Zy^dSlZ;<$`FY#jg!i*`RAQM@hCOTq6Sa`yw7tiLkR)35;{A>Ew zbAjR9Q2t~d4StkjW7h_Wf(fF=;0)l1VF$;66%Z9!i6cLk&*r3ZWFOdhRnO9^dsOPT zvCrSyv_^?oYv}CjcY3@#Vs<|}tNytwanG~ks*`_jGOBYk*EdK9ISHr4bX@X;1hD={ zvw7n%4ksFHigpSd*9JgbzjmWb5RcKvF^bV$k4%35<(|Twi zH}a|B1Ag_SCPx!|p=CHD&InbcCGF>_7%3E4xa!q~|3}rk$3vO^|KsQlqJisdnNM6uf>b za!R`bUV$qcsI|&1%G~J%4a~EYF_y~siFv*)r48xO{ZnwRrK7)TfPrsgv_0ffe*02` zx4%ob+-7KR4;#h~_!mIRAQI-e;}2)EU63}WE%eYY{gM!b6KQkIj@+R+&)tek7+Nkf zBMwUriV$$5aJi+h zQ>XR}I4#aSBb*UB1A~_LQ>KpvUOE^!tE>vX6ns-NX4CpN9*XBb8iQO z_o?#4sR1$3-{q~s0WAB)FDm!?5<_>nR4E&G1VCv>F-^2n!SZ*Nbl66kRB}G;^i( z-byPi?BPmqUlbQm1eNN9l9%Tga&gK#QE>$$pX+CNJ-olP7V2k8~R_E4vxcd{N~u`5U&| z8_z%^-$l6GHNwlcZ{NoI7|0gIVxyP|54fi!Sv7jZxHJ*&D@6md4x$AbhIwmq5x5Zr zcZ7!M(nMjr7kaDgfyOA%+W%w;c&bR)UuB_EgP1*|vNR#f?=lV2LSk||(5wal6Vw^R zgv)2@l8zdncP~Jq0^B`jLk(-#jDz$)ge(mHt-%>b9@3iZaW?QB{TC&YhJn|2%>2htIZ# zRn`xS$m^VWawGW2`M-!D^c>#v4c&u^``bmQV0yKzhHs(k5$gg~m!crUzd;P@r= z47*<+f18?$YH))=Vb-r0Nb=l&64n6Gx$?1o8tQDOy`8%fcyq`Qlq}b-CZ4}agk)x` zL!kE&L(Rj242>sXO<(|oqoAfx@Nr-Wc=_1~-6^ej7Ie8`JWkFEA>_IhJh)YZBLFN9 zWA!>%rH9nQ5*MfhJP6V$u-~S`Sk$csu~af*TLi@>{E@bPM>2vP-4{6u!+OB1Ut2Kf zIA^gleht8*wHEcq0e*x|MjvY%vwcUA?s=Fe5EUY;ePu*ZOhYJpO3x0c=J8vL)SZMa@Qw(NVfSB2j@MjEvai} z^Quh^Gp*AX=i6u>2F!Igg+>q?y5`i9sR@(rY`^yE;@pD4D`%f=)y7@*di5Ez+D~Wa zz4>R`i!Gh7k?{P=K6}gRkVR`R%vl*tpUN+!88MFov4OszM^6xCXM$aSgGah&3SilD zhaO!<=@m7cw6&|G`9;#J#Rrl;*-z5dE+oR(M4W*GzvOOs@!}~0B!l7}9D#OFEFP`2 zQ`1lJ#wnmzpsXnqi2}v`6uda$sz&5vlZVecvVkE^za!1a3v`RanDu7xq7eD}82Ly~ z+21{I*)oh%N3H~Co+-=iv_*n672)xAXdp=`;!4C^WU)x|)8h*Ba)b~`*|A|W!uI2d zp?Bkyn)5_LI6OgCV2RvpHie5YBD^R89Ds~q30EV8dpu2}R)8d4C2UDEg(0|UX&gpC z82!Ey1TdV%7L0s48#Y^>Uq_tHkCUBC3$`L3`q*O31*WCOXc(+T6z#{U50YFjm1M&@R1v4n zW4@EEHEC%nHGMck`t!0&XV2+?4QM^4V62Q2nT{oia~*POtoz_!udMz0Y+7C5@$a@Y zna=Y31goS^5tDig$rU516&IY))G(`Wbz@J2buz`*(I-xv=sCRh%hlx%;!0n#gAyKn zpT8YSu6YCZgF>CN6{pI&9ZH_KjYari3)2KUDI7&KFeqZy_ox}cY1|X>Rwfl`-%r8K zafV6Sok<`d&Lc-5zTDchcvk49j`}2F=Yp;9Qv{OMQ>tBGj!>)u$nEFh#_W9Bgt(Bi zo?9>%6ge70*bcPCSR>LbVKr?Ve)6zL=4^M0dKaM0+4>Y8GE6ZdT1(yY&Yz5WN};fl z_7I^Gm&4U8Zte@7I&L&r&T$d-o8ZB0dvZziq;I~N6PFQ3ffS6e2Q&DPJPQIp6n79@ zYt^_%zjwzr+roQ{9uY$qMMr-v(;_8A?Q@}M#f81$fA7MLM*DY$*n*t8)$Zemxv*c;BkgA#O}ACQ9q_$ZV$xT7f6CD4_>p!sf7#A z*Iy}Wh+>~|wx?AoK!>fMYNV8iVLn=0_t!ev*v9)E*WnT|j{f7@sN6HbbR&s|PV+&4 zUSo5hpdEu3nT=WUO0xB6=_&^qe6~VZexkF84l&JV%Gp81>^HjBB(@A4;JEQzHf;$rPm% z9f5ruq?fQbO|3(wGbJ#SLZ)to2&UA|$Vs)3Eug5ENI8@5MiDCtcXvP4iU09&^wQgV z4c-m=l+wDQ>wnHfYpt|__2X&26sW=JF+VF5B87spzM0%KquDU;s%P?woXLtKFt8RF)@BU2 zNa3VFC|P0mkUPeQin*X6{UOLzGTXs6M3!m0kD1 zrq1bI?~ldoOKtuA-l@Et^J_1|Z}m7-qEroH!-%-zSI~(=e?X-1Kyy|CVgd$=<}Duk z$BOjSi*NZ8g6<>?xv#%DJmD%a=nn6?`SxP@%RO%|&D}KksiZk{2$Z^4?MVebPI%AHVIk$Muav_&vu0dB1i#fEua9?u(p%sh}f(=gr(GFW*MkFSI zARoh3Lo_=tGS=BWE~ca7$r1PR!Sj%9mvzJ<52DEe)pInQ-F%`G~8kE7z%-!7cnv)!5xX<@Iz@!s~=CdCnkASt@&q|&IlU%nikpd{_Yrg z;!6+mIgo?--^cfxU<&TJvty6-MKA1hIqot0zwz3I)bogY`{Uv^#MN64%YFpU`4?(M z?uyt~g`v|!hK8~~Pw!fNZ6kKX^q9sx#N|DEj(+9yv+Lk%hCRKxxqDvUq)C&0E(dJk zxHcyCm0)%g{E@1MC#DYJC(BPnIr%peRj+_LL+TeoY1nczG^5ECSvAN%6<_xNWb(=3LTsM7aj3v@ck5vT2|DfQU) zSu$FkiNI${3*$m!jyr{>Huv=(%X2j`g6DZY5?dw<6spWZSFo@q@EKrnTn_+QYZ0D! zP$Ou9AN?2BIn+5|$k^f-ZbX$PW??WN1a&(<-z*}x6ICAJ2gBw4Ktp;JXzExcHokCg zFLQ#AK@@5QX6wLM_@tEbk;v!_)C@My(O*hvI3&Vly>=B-^d=0RDL%{!eXaxy zBEz31nCzZK5I6}NCsK7koGxQ9aFLTFR>OCALQ{de4Y5fKGozhmvW&a&{kzsDUvm}C zRHR@u1-0_;RHT*}k-=yA98z!Y?An$AqZWv+!Ipt*6Nk&X)?HRSYG^N_bUURm3j_0c zlNYvmxasmtgfIkh&%Kc92IwjSh+y|*0ZTP5T%FSxv+a3eXQ#Z<5_-7dhP|*{yZzF- zEdI=a{rm02ny!vRY6fsk3#h-16sWs>0Y51bLq#jm;?ax7^F~097n^k)B+P9X zZ2i3eJwR-#NJ2lpz7?SsllZ1l2@Sv1!@ z(Sp>YBH!SrZbyt&J^=VZA%`oFK!MV~g2~2tTT}r}O$bT#`RUlxCr_SqZF@EXNqeH# zCnK*Uyor*MnSlrS=Om7;FqVMU1LHw89(|};tqV;7y1E?2=&o~-lrNhEvcD&SmP1*# z;Jt8o_3lxVsTK)_!J#BMXp9|MTDiK@&Bwe#4B^g8I-l3H#} ztuJ34O$XGm)E|EBMJwO0zHw#SKU}jyTm!(6mX;P+wzRYyZ5UiJ*RdjJ&f>V^d;a?Q z_6lUge9o?esnDD4w>#$GA@DDHetGeoqr&VIRc0kTXP5~1Il?bY~$C#M@3LS`{tBEeFegnv5@tIn$6nb#ZN}X56=@Db$UWvh!j5PoJC_; z95u{_oZTGh(9_2P6>%20NlziZS40>5_Q9!?W-UPuG$XQZ^T%$U&c$4T5}VUlkXQ!0 z?FHfae)_!T(?>*M)R{oZdYn ztbXMp8t0cKEE-Arunnh9UTK1F-IC@$0R4_ZiTk=Ps^UiZK66pYR4Q@RGZ**Ru!4_= zraX`D>>`mZ$hH{Al|apF%|O~5sLIMpruXj3Rg*V9y&U)7pXSg`87pY)TuUX!@fg>L zFF#*#^0aEfFU#A4E)2hG9MKbBhZkL|+q#TAKNun8_>8cmS8#~S4VLUnWyR+Z(+qHI zV9+CjH$EL!7Yw?6Wd4F0k4sCS!HCqNn<$0{Z!7%C!RNwf1U)xQvx>kY0X^PBUgdER zyHV#Fgv@dzybAc@){pNP@Hb&{#0<^M}kUzVZ^y!|*tAB#po;{nm*E_hb z8rD*+tYkGP1NDk)^`$D6iorTQ!DS@%z&Qysm1%_T@rZRnd|<qgjjVz;6m9t%8kA@U1tk4Zay4-UdR+s}W zSeF0;V_;hheLoy4rJ+3dd~3gty;2h;9=*VaeS)QqJovEwnfCUT9XdOMr^f%>&bDl| z`PRDXKrlwG3^X5vixwyu4-oSLzcnr;W}#C>n$B)noGi2M*F););v7CTHxJc$zSp+= z&Of(2$36#yQ1|Ly&_u7T?bY|cpAwXJy@M3d+wNq}da?12DVl*fNced8`EyOpo`j|2 zt#Qd%rH-a2SShfbh0c$SlrtJjf{72EX6Ftrf-^LwOPaR~cMres219e#i%TO9E`8n! z|9h(^vD(r~rjJSnAkJl9{p%D$CNZrp_kn>PQh^s?xZsfs{U6wdMiLKoLvgC5$otGQrKPI{MlT zByqY?MS!INIl{{i%nmuhFs%y+c&chv9~jGG$= z4*Hv)*XCL-Mu_k?N_AYocW2jreJ`t%FUjqU9C-zFIopq_h42Y_CR->pv>u{5S&qWT z=g>u@xvJt0KJ49od&PxEY1hS5#S&nkCy3Fi+osxT#9zRIl+PTKb6mk>=EM{?$T>P$ zj%c^+?34vje{3aNa<|qttgHG{$~X=Yh{u9SdJM_Co3H=`(jnmBZ6mYd&=eO z^(X|Px`w(7#ufRC!H@w(S219N00lW}2m~{@j76>Ga@-rR1}-!m5xt?~;PwVzLlVhs zS8VP=XG5tkAESk7(J(Y+7*vAL(*bAsv=}Id@gmsS?LUj)aoDbz}boNGMyGewuMm+)KEp5bVvj zjX1tc5$ERU5dR-gE{1#Kwmql$bh%k0TtJzUb!Qn}f#l};&L$Mw7Bic zlR(IIn_&1mhmT!f$}x)+1eyqibR#%5xYO)IXNXyF6{@4x!}M_shkim`I8>JX2}n>^ zkC&mx@EREptJ~hJeld{OmKYI%kk*yC>$=uCd!*B3W7I(1!I-64EqPkCDjIXO3~vR) zx5Lez9iPI_Z-5DgZBNo1Zy;)Lb5OfN<7!(+;ZjB`uUEn0cRW)?7^cY-=#73XTX?KI zO3Pv}TP+kI2Ju7^B;7-eQ;0Y!d;f8jk5^pieX-*`!UG2XOW5r%mO1U|eF`nX-_=42 z$!$CnE7)YB|4%r+`R8K8?gJBTlJdH5t=tkA+tM<-sEAepP`qMowR@BuKAq9%EW@cW z9HvUdOUVSb9-cvqY~^v7GFe?|)RVWrZQ8o^X~@po<^S#|IsWWF=Q)(5rA0dUV&TH! zx#ek|eGvzCKYN}%I*?&eD&kRa3FJp03Ysef%>F%{Crb_(trqZP$k}`6<6#zFfr&+} zB2x{;v&fs}EsP%1d|loZ)89}YRi?BqX;)QGcBn5#cvlqhSS>${Nj|>EjK*a`up(@% zgwYJ(bolrpO%O~jq<%xJKF4Zq9M60mbSV-C@d6O3A~F)-Q<^$t?54OoEKAN?|NSEC zxwHr;zS32J`X!7QZcZjvjl~0&Y;d1(I2pSgah1?7myBDdDdOzoo<4H%M~;Hgf*hHT zXo@XeLXqsLy%u(;rDfSf?VPRp>NISD`LpBESihRGze~2EOBNURFuHixljf&?tr+KU zDz$KBsjSt_-T%|-^6MpAKEA$BAoa)p#0KVh{Moo&x~@rJQo6_y6Ale+f=XaDrYC?o zh7AAY~g- zb0&;*$o^K3f_O6A>}gm+hzHwS!R>GZG}CBDQfE4`-KG1(b7nG1mpF)_$VPY9iVq`Q zc%y)XNbC}jM~s+wKKuxrAmOI>*kvJw%t439g6NS5B=5mNfk2<&39lnUgs_DQLS5NS zEpdR$8?)k8Dzp&raK&{*a}Y_BQq-`CYxuFFJO`b$zRrXWo9n4S8wsTzU>g@%DwNLU zAUu3f)?gc)MqbmtG&tnsr_S3oL zO>>`b?pYkTkX_L#b7J`GL>(z=*FJZOSsw)!{k5$9G4fek{(SB&UG6Q%!cen2g2NIYK;3?$VM2yQMPp#*1>%L2 zimXuZlTi&X?||_$AlB9lVxS8a091wPPL%Fhbx%|UEZOhFop4pSQ#Im1r@MzNgOO4z z1Ho3|YJjYD>vPsxtH;e;5q)9Hw!ZdzN%i-JA3y}TCc^crh_C&GEij`ph3^kWws)iG zS!v!m#^2XUk@HMHYAH-sa=0fP1g`LST)=}TQvL^?2 z94jAL`Oo%9`1PHF_Z-FU(c_4BiOH=a{PCXM;CyQJ*@El|X$CivgcQb9VzvVNr)ARQ zd1Pf^vgeWks=fsVdl(}!b~9+04d=?IV2}n>f3Nqg-{7V|%P2~)g?bgmw;Mz!Paj_k z;7SyXBR}{*{yLaqX&=gR0JPw5a0x8Uc>ldw_ISn36`0+O1KKIfX(-JzU+SON*g03L zJ==Kv`pAv98xlV_n}JFR5?<&#TeBVLsGF%*Gxp#4ZAlt;DSRwjq$Y|YZ#RvAXV3R% z`(0SPwZ5}>$NTHx()SVObh%tE->@k*_Soc@(X$ncHc(9LUAZ&?hm(|?Dj_)c}CBDRBw0BQjui;p!DlFinfjJ_GX%5J&#oprH zSm0{-%Y-hHQYhwpAzqP=+{uYKXv({&#>>mI;v%jxev;7HaLNLfgG5EW7NJmLG~D?O zw}|W=897Kdm}MlI4+tHbTz)!SdN}a4zO)7+8WkTl^nRE%7-sSX$S=rF8)72?IC3SO zp+VM58ejrAxzbGVh@rv~x*P)9vVboz<6-1b!Avvo+QUeYA_^Y(fQ=%TG>s5gdob3Q zFzeGLps_Oj^p+X{>U(p;*VGRpY%ylx)~K>Z{zDq|2^0qW>xKXOJ(hs>24(>i9C?MY zkIPshH->r^E@$LWmw;kr}>3p+%f128QYY=yi~O+PM{WI{)wm3irNzn4Motdf17soX3MvftcqK|KKhJZ zq_uYKln%EEYLmYMj7pXDC*J-GC--hF(~%lXa(2LNRI~EyCEwl`XCX)o@@7&9vy~~_ zm#^KFzf!+&(V)XB`&z)|FVhNhD*#a)iB`#!I^b(RE9Lzs{*Kn^1Mg2ihk;@DNJE>x zYgW>oS=(=JOB$M$@Vt4{lefJBlhaTB{l!Ot;J7%Yxv#>!V-8a5YG4kHTvv2TblAs$ zQU+H}X!fA(!CMBMFQo`Z`QSu>3=WYxHv(p<^_vP!Taazy1j?^9rgP|u(9b{PE8zM~ zJAEkJD!RD!#p&Uo!RjDb){mUXY-o^;4Q992_|^!KE{?*!N9N1JBzWao?_(c*UAhnS zt938ZUp_eW3pIyF>yaUV9FsLJvyjubh=r639CWl+mc2+}Jez?hnW1JeSJpDBLNApR z&0o5!GJ9a9e4Oa#udTmUWG+5d*7>1uM?*-4i~ezMQOY)#-^OKJKz#+fWQa5EGARmy zj7CBYWVAeyRCJ1Ve^oRuQ%&OtA#c5$<=ws+f`h0HH!ToXZg>N?uTjX!wE3}+6 zg)e(#rH;(FUVwtijd*Kk5ymBoco#0hbE?Ng^w6!qye>{$k3-hx9#c(vb$D>X)(zrz ziuU)6#=Cg(8lrsRrL$^sgor7qWipr;0_??;4Rgup>F^got0lVi8&;-=8Yg1o0x0dx zv3M(~7Ab5Tr4@fOOr+!ZQjRZc0(!LZIOf8wYwKOL+h<;$VfcEr^S%Awln1#4QdmXkShgbdA?FM1;!l^2;Xp8fP(7e9^6}6= z1EKaq9$^4W&~ZISo<#!*AlI7$*o{atBwfRGmO_1CqVVI?(0Qg@m@qJERsj{lw-YJ# zLf33|1<0iqbSL~T!VStv_8bg)8aBbcGPwdnfQ>(U8DY~>aUe(%8Z0o>md^%b0oeUl zrFX|`5Y1DDlu!}fhz?12JNl4PwCpuFwpjI63?bSHpTzvC(oZjVWU!AlPyY@H-$;t;o|z zlbh1G8dwL}a#(;kL}DIT9_jv2hjj*#zO%wg)!%I|(hvQA<&f7=z8`ez`Y8uhkSf$X z{Cy@yfe!&S+#Za*bt6wKE{!|L)-WXPV8emx1lb9sUS|z8NP%&LPZpZ`EADZEmdkOn zgI`AUT60&EI|uVWy>3owXudu&7~6b)=84UBT8^Z!5i`JT#2A>T1|1nBus9PRmEAr& zyAi$1$U9(c**8|WAFRXlfDaG+I!sa(RJvtWCZOLel}d8+$;Ixst;L;0M$q0j@^@c> zUNvR+q>(@Nx(F>!SI<~nvy5VL+>WHAR7zN5_-ZOS1tShY$LMh6(rLk+=3nH%ZW728 z0A@c813yKBbRMt=@CDyb3}ZzNpAY?UW=du4WqhmAG9E>v3nZ`^CHR3_rN&570V^so z55Iy=(v5k48B!a8Z1`O$X_al=xbee%5SmC*^sS=nwvlt~RPk#fv^CO4I7wON%H15M zlhLi2}@-nAxio3jQ_zT0SnoIc_OGo%h3l#SYHH4^-ZU0h!)DW9cqpNpwCF?3Qw`&|T$j;04-k{k zJtA4j2_XFG>M?wd%eT7j$v|$ARg6cI#>>A8Y3nH4d@toSv6u;0Ik*OtdP^%{{RtAl z%2-I7>H-izl!A|gW0@Le91;C!wh%B zp6~3Nr=6<>QD+)^V&G}9RP8Spp~iR4x)Lm}dESbRQNM?8+f8Chr?Y{MKwE9UtT zoc1>;XtOXSlNjK*B)tq(Z}0S=qdo`Scd0dGg*24=p$ zhQyPwjCi3w8WX7+^%A4q7z$(sz7+?%u7FVYXTgCdJy0*_-}kWLav`DBk1{v>NR1nL z#iDd}h;AaL8Spl8!TZA(yiiJ+;K?}Bf54uNzW`E<+~c~a$$JmwlD`aoxD^TEAr!XNZ7#muw9!sFN=-4K*Lli=iwD_7K$oC!O@o5<5PG1rStU7-Q&o*2pL)S8Tf$CE;h@n7`%9q@n!sZUuqQa9HT`w07E-=DIr0IL$-)s2(|uDnq`#~`wjYhI!Nmm z5n{t4TMT4DbQ;38K-#uqNF(Zve0-X>h%RiIcn@(=jl;(-jr_a3&Chl5*pO3s|I83d z1-$w(7T$~nQ{htA>uUITz&fnk^y7;ikNa%LawR+l%a4W4i@~FyuArQhabs*N==P|l zXmmE;8_t*>xr`rq#=9xeEW`Z1y5M_DabzF25AfS}B|=s2UM)PG>W)S1Z&isu1b+JpsPbF}i>%#uwy+ zu7sKP*abPqJuAID9u|CXcy@J4>-Tqp;$H?k zJW4QK`SL7NTrP`j$9!EC)OL94?!(8?*49=hKa@Ztk5&**8MVA)AdAoESMZc7^1>e2 zwsc~)iUzkt;(?$o&7=v0!Y^NpwRXd7^uITrZrRzqMFUoVq0jKatQ`1R4?gdhtIv1c zD{0Qdxnzyl%J0*I#~BNltZ~5#-l+=Vqnb6kS@PjoHhu9nhAm-+OhEB%EiD@-BaAOj z3$ItSzq0^XSium9hijx^%?+TqW{`&w9ln0V>O?Ht<0)MkED{DEmXk8XB+@k^3OV3*hCI{VulT-!?vY z$?A09)Xw?jwNvZx=l5f!=IUASh=ADHEaCyX*EzK->o*-#tNU#hK>^S+CG-cb zAsU~b%khLPNxRp6>=DUn20Mh!VfXiHU*G_M{t$FpAQ$SvC4U3`@3gj!uS{RT1YIgy z$$?y#$^le2RuCE8yrtv`k%b@#slR$tXX7?_n**52S&Pp!^tUgHYrCr1zhdMyf&oKw z!iDB%M_rE8MLC}S4SA;H9aW-bLZ+Xu6={rbqSU~t;@`chK#ke{W;YnnLs8pEcnDH6pAXhPjETy_02j!Rg z0=15c&%k}Bl&bn-JP-(z%1E5f<@v$KK$XCYc(?hxIHTa%#g8>P#Ezi{ckOOqa!q^y z&ZRSuE3lcHauUMDs#K8}{^PyFFR<8NzM2=aC}e)hRLsNp_Smf$^j3H@P<(zQ4Ff$X zd~MjtnF6{Rj20OcsiGKS&~%k_yGJZow4}pi|uc_hmqP0i^Hzd>DiM$saJy8WNvkW6Uo<-?F-Gi&yU}uimr*yXV}`JfE?cS%G5+d6YwJcqKz{$Vjymar@kv z)jeE87BOb(N7xIdF8B|A!$4=J8P;G;&_$r?AhS}F;8`u7qNDqX(OICv=OSkte;JAZ ztUz^~Y6v7;8x$EHVdR$+K?fQOms1B!hO}w&dA2z1tIPK%{E1!Bw&xkJfWCt$^X+tW zHci?Swh;7~$tdi_ReEbb31(xuHXmNT7mzF5vI8(u=VIaF=HmRf@o7(?{qo@wRxMqZ zglN(7!ygr$C({tb5Qw9bX?ygMlFK8tL|xLaDYUFxPUr zT|y-veHNEzZ_7{a3RqDZFKa#Xv~TqV6n;c0MvRSL0czPm;zON;F`#hwBnf9_yiz;U zR8f8kHD@;Mh3j?1D3XD3;Ik65%ZGq3Q3Paic zW^*t^IEL|vA@bT?Eob7q1Pghxt*&6P7?X@NgD+ODzXn@T8tCCDU39alv{bqc`b?^cu zel)Rh$DWc5H+-3U24{Bsv6}efXU>(#;?_Ak3?m9k2i=}ab}(KeE<#&s3TS37A7Sbo z9_s|E2Mu58O*g8M=HFN6w16(x1(*}gw4d4rK$<0bC??YL;9X%2#u+dWp@O3O)lxjT zSXt3vym~Ma`In0!mLlL*hLe+101PhzwM{er3Ld;@DC-^`2{L>ND&P20pX50T3NvL~ zuI1qb3!3zxWA2lL+7ex6*z1++Uw+_jv2}nCnFKWk2#g{Rx$mlIL^*@s3}<`<7pp0X zJq9CQ3+VM#^QASm7jGV!^5R&*#|4Au!kkOEGz%%^cAvkvxp>lG_Rh4A4h6!pYUkrO zMf;{sWr#h!kNgW{@Un(wADoQ2`75L$KLi;>077aGi%>l^Jb#3#>j+x-iADPKG zf=8QccmvtEA)Y_di$)d2a5ryc6B|ZJs53DyL&2ci?T3CriKZ9NCwf;bm0AmFM1(A& zwjK|Kv-{1b7w)~hAGG}u{BSNIKr*PUbq?G{Jb&#T`fEE8&4pXZHptd{yxOnzeB0#J zQ&;})G?#bzZ7*J|zkq&~0@q~-VKlb3F!dz`v%uhpMjM`EbOy-`oq~=9=Q1v&+YXfs z&Q)asjj#OjcGxHj1YjQGoTxXrXk;T%q!w=rA_cts8{mX;4=i)?g#6!wf)~Q)rXO_> zI1oS|f-j6(grd5QtUGl=7Fn)1_uJc0?k9_Q}F^;`PgO2sk3 z`m((7D5QcxipSdz+)?tAt`(pI;DQ0k<$`%fex<)pqwe2^9SU0lU_$t%o}i{`M0yZ{ z@mXX^BpxnotoSix7ZZUviM9fMXKX%pWxQ={CxuxF-l#Y5$I;AjQ%4d14j>vF!}4mm z0Mi=XhRLCP)Ej)=ZZ-h7{%jT=NR)r*P3SNj<)n7vmZZv!wLk%oM*yM#P`+JnukPxx znbc=CYVW9E*xZ_?-$g!!lLc6Um zLqD+@L(!n(pTHXhpENQbZhElGK3}qRb!O_#?i)jB`Y-Jof-u*`(C+g7M$hNob%~H? zAwsJ$+Wl4h(KEv6$ssJ1BGpmQSzHTPWjhHNG#CtAMM7XZ*lQE;tO2{E!YL)iK#XGo zkIhe;2B3SiAE=CE@0p;)adHr$+W_on^g7XlD#|>xO5$j5jfgrVgD!*L8bvKa z5uy{7dh-M@!KLwR7x}uC3WTHW8B%J(75~-p)%xM@KOQVsgbldg%oLv?SuS4-!%!zWk^cznKJt?x1g%|4uCg%w}N;Kk=4WCW{0BVwrt0tMtA zBYp{#?zSosi(#Q~qEwaA%5@(e9+|%C!`YpV@?oDX^IBQql8tLZS$_Jtq)}7#U*#Cg z$z!}!BBtwp)kKEk@#WLVV~cgyE}w$4Ek}ecvVuYb?$drfO^zFfxfF0X@`(E1lHl;8 zSQw;N0;wy_jJtVy@#m7y!|(q4(?Bzz0&?PQ{QRyp_R{UoXL8qHp0~g0;ls~Ku7=YX z?7`=S#K7vkU$)AG`;9O*z&es+o=l|~(A1k?-Rl!>|2%D-Qyx zTuU)F0d*XF{2`)SNQyq5PK5YB;0k&&Hw7X*1;yq{a4I?$!0tahW5CP-2BkfyD_{r? zlQ;yuiovfTDr@wIMTupocralXqULB5bIg~D_XYo2GI3A1uwY&X^oDOjlHM%z95e@2 zvU{CNQRv0@^LBI-Qvw4x6!#HO!rZ)i)Ou%Hg%EBY@|ngb;H56s{QCIh$@yv7@Wk9n z{tih<2}ydFKJRnCZtUvjg=pE0d>>Xb!DntlQL=vRanR?HJ+Xb+fmgFl-j=<^jJM@pc7kYktd1>I^OTEOi$E7FOMW=BYaNF5NdnMT4H1!Mb5kULp3~wY8 z!5PMcm!FOh@ES^*VNR#`G8j%v0ZaQ)Zmb@n1yLCLLT(QF2b@yK+!Fyxpfr%!BO@kt zw#Yif>X#A=hZ`4%D!UDpL4${aZoJw*_r^u*d#=Dh|yW%|TXTcj2&<{LtxRODo)%g-(=!o<7tnHE9uIpR&eGlukqXrNWiD zgmSb^X0=A|yY|^c{y7^a>mO`J&RJ|S3Y=71`(yK)m*IWiU0PB8_rbjjU;ObfFJ!l| z!q{#xT%Jog2-(wQB1Jla)TpHT7?mw|dSjvi)Q$>m97(rj+2@I};Rs@Va+7G!`3wWTt;K3xMB&v`k z5N83E4}<0mj{I*XDj8MD2khJF@0!RU_3+nyT zPcR|UbirKj)`Rn94{nwZ?YK2`^Y##SgzG&q<=s2Vy5W>)o&WjM*}pE{n^WS4;1nwr zqM2}UC=n(TJ*cq~bf>W_z(*8eyGQ@8E2srm=<*#HNVx_NLS(?I+;q7??o8|sE+K&C z)>MopAPqZ=^5g_xX?oNUfP?y?0Led?r_ysR12xFouF&5EMV#2Rxu`3^iNaFEz7{uf zla0m==t2(Uy{+}>eGVbC-Q%uaFV2t|t* zI+gSSM=97qi6BCf)`NC22W=EIdWJwcICIGG>ZxL@DbQ!f;Dh)|1C|WT=4{7M$ObUn zWhltF0z;4TZ1OGNg0B_JqIF!if+zx@+MipZCa#RltQ#!5RN`-PXZC*k?8CwSY4OJi zKN~RtG&u2qEbGUG@wW;Ct#!>&+e=o^poKBpRXZQB&xtHJ~BDdRk)p*TpBnXU>`4p-`kO^cr01GOTgw*>~yVK9|0a zE<>a7HAB($=Fg?yEjsk{ui$k)A6A)E@nrb;vC~e*c0iUfJ9k>f^#IhHDlBZ|(zN3a z=q3W^pFS2*&9H-|q-im(bOs*BkQNI!9`)!Sbg>9^UlDRMxXvu8%?#N;Nsk~1S{j>o zCasRItNCBApdB&@roU?%P|cpOaxKF9S7#*o;F_( z8zS2Pn|~k#;x{>iY`}X-qzx1v;H~H3Hx5k6Zy@IpqUF^);>R@w`8MFtS9M?#n!{*8 zEEGVj&GHK^QB=^bE5 z?FEL>h%v+!(*2+`{F48v3zK-v>Tx0JUJl9xdp1`0aop1NWM?v>k)4wPuFwB1V`TVx zVr~D1g3Z#OOSL>})YtJrJd~pi!akaxJa83x;~)aT@D`jIItStK7%0HGK*4bX74oR1 z=+IP+0{$+P2(vo~7ZOhJ;iyGC#u-!%4iNkMvn#|rwg?>IAF5{uz7l-2S*(c*rD&W5 zoM4qopt=%ziI@zMW2zY`>mO6My3}(0y6BRpRbBKP2!GkBK#nC!`d^s3R z#7Lnqr(yfy{)^p%?;8g{HQsvrey(MDX8c4-n*${S&}}AX8RwdRw) z&iw9wUi*eznNw#o^p9TC-BG$Dr-71%PoTp6Z2cRO@3d;mL;F$+%E~%EZGJiN+bjIa zBj%;fE8P%2iYJ6-)M6o)BA1E4f&vUs5I#EHU4ls#o>=S1{Kc#<3s@tW<>qXzSUxHK zd9VJ?!C_iK?f2ckTnaF)U=|L{f_E@lmfTS|H_E{zH^kw&4HI!YcKLc(=61pbIvhGK z$OH5{{Gg+S!wwe&7kOIsc;9vTYT?cW&>bYA>

  • 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

    fGV)^n z$C8|ZSz12Th@>eB-kb6raPOg**?OlxP75fEx+%pn;43foVz1>M9y@tEn#ku^p%ku(ouOO!&ra68F}l62P#>psT` z9C{j;>+&gaZ7JJ>uI^aI1-4a&wZyW62JgTTcH6bDYWOYsvUk z0c=6QBcqlQ0WyPLkFv7U1ca#4SwniQWcq;)8nw|k0{oHN-PGykgVIrrfjxOf_2XCxa@yrj3W17Vjt~I9(&x5hQvbU zOn+oAh-+oSZxcy_AaKfv?X(6(fb?9vFK?{?idnqMFNoNln;)yD3YKR8aW}nUC<~6_8)k&?m{8b1AfkZ5^p5+^?yud z&xxBzsrf`Y2FA&e=kr|T$NhdcBUuGyfO?zI#7)-3`8(hU41?0}$vhdEQHV^-aF?5O zctOF%ZR%uSHH#CTBNl3N;R!6&%lokJKc+iuRywS7sx_3~^EGg&!lm;rHxlIR&rv}QvRvN9KPxU-0 zc;qK+T}Ir2`z&Qx1Y$%AK9h6UEsBtdl>x+;Ot2Sp1T$D#Y5C>=-|u>_ys=z$SU=Xe z!$XYzM8-NkHZ-LJPfjKbFQ)3Vq=O$h09aG0|Hy!2!&I*g=Naa~eU&#Me#KESXeY^! zSdy?)A5TuT+>hEk;Wy!7Dv+!3HBVuli`0j)3#ja>-1%@kbcXDfER)SWt&|sEMzZ;Z zN6)oXG*Ns)20E#FY(2P(^2`hl1B~@B39#<=P!V)#<%I^Os`ZYi5|@0c9<)X56z>*7 z7Ut=|VKb_PJI*bCoT9~!jKOMhT1r0>*-XO(A`pRT3=aYcVE`^-o+ccKW=0}~Hex;| z8Z`$O?;#=9NkgAMAm;>e%09+wZ|U;3y|<=X8%Gr-Ye4oqz;X%Zh7%#Uq%9CW&vbC? zC((k{7Ye;VCnfd>rfkfRU?Ioyg-^Edy*u&a zbHi|gBv^&7Gw$~k+_unJTDELNxCJ$Bq$^&S?Lx;)0f&ulG8(urt$#>+wjby%qI0od zDL#ytJqddA!UJ(^If?Bgpf24{BZJy;1)M<(cXmzNGRw2_)R(wV_cged0=bNWk5C4d zf#ir}t7(c`=hh&}2nKDR9;DrL%tweg^CQZmDsUl?GFa+!K>{PyX(W~)t$dC;$!b~% zb|=#5JSD5z^3_=d0r)VIu{Gvzqam!=FPM}N0g9sL@egQa2yjyieNc3!;sP1v5sx|) zg)i!kfCd&;GjoTl8M$bqV&FNvBt=p!4S{_-IDFZ_l9P$`Vuvtt8t}6YnpkZZ=gK%- zp`#eM=CW0H(+ej|XK=sMR9(F^s5lxhs}ocnD@MY|JinRB*r4X)YBqzk>-6<+e)23& zfycF1!F@j0{2M6(<03WKL3~*GUZ4g$Ys63Q`zGBW%8IEm5N5EtyOMu1YX-8HDdJo} zU-cBPV~Axlsa1$B3LwXOlh~iL#s+|JI&rgukU_i+#+b9#(Cv~lU8|RhS8?bbJKKSC zTA!g|!9)s>pznmYajwr1vN5i7aQ9q6{3N!dW3bjFh&|2VoIvh&R17?A!^0lHZ%Wv3 zJ~P{?mjGtJ=_I@q$fvpzSt*+%yOBqb!Tjy5V?bJRvjrXKcKEXQ)axlT)f7SiulIJE z7m|m9L`fov3`k6ND5PJPx<=d|U+cGZik)E$3W)PhS(YGgzd_1$Yw0Q< zp+wom|FU?Zc?9L(;ACUNbo`KyKh7<=YSppMO;k(5%$~_avt1=g)CuizcZm5Y@SPqbq4&SJ@t6fu2_Ga(530NG7q&qRl#c8W+Ea zrRvQ77yhw3TOKvPXQT_#7Whg>$hFU%dYs~DfEUfv{%67thQCj!(L{Y00^&gXi70}O zLE6B0K1JvQ^%5`5B^1pNV61zDnU+FxRh!rrbj<;Q;fb^`eNEOpyv}kbS1DFQDf4zL zl^3;U0i0{9TshZ}tm6o+)|Nwn8Tgv#dxC*U@2byL&8i^6F*|;=LT`^p!O(M4>sTdr zQK1n27yCrr!3-g}7LIEISj%hN1JUt~EYM9Qrn@e?**f`->Jh9wbcs4;p`|i)4rleB zAYl4JZ@&$zi5PAHWq2}Aa}!+|q*B86|5DFjwv{xNN`R&sS@Nx`m<$fEB~~T5#PjSt zPlT1{AbJ3dHrA^=Gs7I~%S*=;VKnYRD&m}Vz$XxhTyp+Q8JUpGt<6>y#pcD)q@jp^ ztPNqe2?$>z2nyOb*c7P~)EB<^!7vt&S9!sh&ae~6f(Q6kvC?+lwU-a;QsuA0Q*$wa z*?=L6Ouxl@8&#qwx?)5A#Z(H=pIJ&2P`7h4-$6aWyc(b^YuyfT=_913ogZwVXkDaa ztREx=zJ}E;Kt?uc3BtI7Y>5nchtQ!3v{rK6%mU2bb6`%MBu|PR5%uL^=pHa{_(E@pc=Z_RUZw~yZ{?_; z*MJ8%0&E&TMKx)#BP`Dyjz6DNjUb9ZP^cF^TmSgCa5cY0HNwNV2C|~XJz`SB9A&$v zcesU}$-?BbT-ZfB0ARstNI?sBgk&jqr#vf6Mgkza3*JB3g&q$BZo0td#=~qfNX*HS zzcaa*=ufvpP0#Ck(|$W}F&UUmt|5~SDu9tC9-bb;at2O6r+1!O!XuzVr~gVbgPxUe z8BuTDyR<5l6C5z05Vs!k2b>l4BdiciMk#mNDUe9JASAOrEAWT#{(%WX`mGZ@>H!HM zFePM_Fn10z*gtC_<)5I0TmU%q7mPv5#Aum6TEUBCzMJnm2)~cRiWy@u6?TMR0D&d4yQQe`w}vv+BKb{~`E0sp`5k*u3*i%5yp_5n(0qVp^t+L@wa6v*` z1~mZ@PWLym-ZxQpJ7Mhcb1Efhw}IQ@6vRBew`qTmy}^Y4tdLrvM1#5 z@&YTFxyvLBJl8C;(b*Vmt!heK=vLloPBCfds$}$*w_ZJPI$kB!XDhv5;73AUMVKdJ zErSaw7OCmSjHX&72s}Jv-9+XJf}=62bajF%(Ab}lgA6X>$~FA(Pk53R5hT|I8Yg+( zh|_OGLtGW%fPHcL1uljuS16(oh1g86@1!!=OK%V(JI;?@_H8yx=uN!XTKawI&OTLm zA*80Tn^2e#B02RxBlJlhTDlD^mmjbO7zPv+`1(fNhDE7olc}AuNsDC7M|EtpgZ=3i znK@|Lb9XG+MnIy9P+I@GGXE7>jQO6xQICp|9Q}(1W#2m#lmLzebYI!>)dum3E>;Ry zj)l3941u&?AZ%FgI=0$hgtcgAhKi{OWZRUF&BfJlbsJT_*5Y$K4NczgZX-6#LgOd@ z0PP82pugbL8Ilg*8o9LZyS8x#>oxta?!?2ESB~vYASh=mW`1#16Y82S?Q25seda3$ zC5mR?I_$$wqcOsY2kLH)&9PVOrs);ZqV8G)E8m@#0 zh72XZuD4+z#H3CY1~Sl1=~jb+lmRxDe{5S@zahLem-@o^0Oc`8SThm}%dI{(EX1_+ zq|S|?R)Ca>??(tQ)i_Lz)e=kyg-jy)4G024=D`mx`^LDjjS6z);hL%!WuRfFI+f6{ zV0!JXn0gI1{L_gT&HUwTK%IoSlVu#7 zmKWz!y6odEn~1_^@o}(V+D3d4qtNo;A{m5K z$28^kTd7L&6G+AVUtH;%9k<}9{W_!S*yb;=9szYgWo057eOU^-_T+r)I*m8^oe(7; z-!b1x??+tJT}e_q@({bv(DzSxi|mg&r5IyUvH zFQHm68hJVbt-&JN?rs@TijnasDV8&K8l4|9Gje6gWgI3ysWb|5J2D5fOX3RK$mQLo z?E&Xp7eoS?u1I%uml^Gf$9jjL>u(px1H=;gE*qZLl$Fm0`HVYY{^_frdpkJK{g$)} zh6j00YkN8MPy(RU2ZS&ZuAU?I2qs$yD11JzJ~|B=tW2dW5*dSx6bGQK|4lKpL5-s_ z5I;WU?D3OZ^Hpx=$n;WBId`WvqX5yW6~-zcND|h;PJVH&rF$79FP`KUK0Q;+GvO_A zYaqB$@vpBUmlz~J=8)m~AY;;8K?kGgZxPu;#xoK1#ViMF6a{_-XD+#|&|;j};C?|2 zOd{0jI4}}KE-&&rKLp8Ec(dSYWd%%@t%h?y;0t|4Y)X`JGNlCM*%RW=EBLZ*1}{bl zR}>89M5gi)v{~vKkF(&SxVwA*Vut`AvZPrgmfZK<9OS~Qf6emVh1$IjUq<iUA5=h;pj#eUqwY68t)kw+9!^f1dQB${XL<>;9-Vsp9q}Gf zGUS^blyq~0BI_wL8CjPmN0T=($W%LzSH_#>V?-6;)o~v>wy2G~B5cB#TxuH&MJla! z-2QrPd-M@=e;$}BV>%ivs$C&3fFoX~jbhN~uGyrrN(*)Q47nP)Wg?|Qhn}9;O-e}R zK5{v}c^Nj$dIFGkk~)Ub42AmoF*bZcdGS_0O^juqu=Wg-JeW%J2(ON%ETjl$4YJGM zvPLkB;?)f@6@ZiUh&X(i+Y=w+8xAmlK@e#NVt@HqAd)F()Y&Q{U>G~83^s{~&I3YI zY$oxCyuv-2R!;8pPU|zN?KH)xdr0HeOU?PDZ56Gd5VKWn0ch2l!KmQINp$NY;S4rhe%d#YCE!{<&uJG zf^SD0eu#fjBcY74UdD%jT>3H)!(hLml%t8ao>fcZ4FUrVM2IuKNu!pndajLY$r@C~@abwbiBTge&AqI%hElK^ zwGU0-rG9oZ%T7zrn)t^Ugw7yyve#F1{yi(uGgHP2zcO@JR;S<|ECZvq{ znLRPdg8xjH+F4tSY0IPaXVn=Xb8cXLjmYnx^=b`!OLRp+)^K*3U?RLvcGe*Pj0bn z1WDNedW9yQqE7x&q(gT0&s8UQ_n2l`v_D%&oCcCrO93gR_CD6&B`Xr=H|mik&-S&R1-Db$Z)*}F%?;(GZYCs zxFw+rV34h9dq!o`VC^SN1o!u_9NluH2ldH3wWc`AOm{5#d=tqZc7p;Bv#hfuj1s?f z8UCG!)H8)gidDL@M$s38Rte4(ED4k(4sJ-h%+N2$QqogAKez0tQJQ^20~+kg>Ljn! z;989q(3F>Bz!6ZlAb@;yCapvFhQf@vEZVY0!!R(9-!LGSCeX~4HU1m6*f1;rS{eJ9 z(W)QG3~y&dItzb5kP^2Ds(VE#^?3H^*^OXw@MJcsRW zOGIu}V7zqa#zhDrDI}|~SrCtt`mF-beP>O0y7|_P8X}>IdMDW=djRo7{Orc)f~`iAT;d1V zLB5M*(~OmZ+N6=!j<(L9vO?HfK%tnT`uP%K+r>_q((ZHrz0XfD~!-+>=%Ej5l*=jTYXny?7Sr@ zzufsW-jV%PDpSFL%=TfEg3s2Pzw98{1eQN~D-QK675F^aBpQ0QUzOa#dV!v*$JN{X zH6Vi7g}f+nm_?KC*Qm3jyji4JpJyChL#rE8Xyfn0Y*rI)P}L_r-EuF-sM|w+t54Xb ztXQsR^Z#_m6#L_9fnNw{j*B={iO(R*a;7TjL{f2ZH#y<$IC)++yIcBU>d}X2+Z!zz zu=>~FmGpMLr^%`aZkrOAtz3ySvqK}q)74_)#n<8iG-O(sW?p=rMjv&NFbM^XH|t~r z`M!4M`_ePlpV15marMgz$&TXCVIl=suS;Yb^=+;&y4-LUCvX1bhw3^p^ac5-w}qSb zRU(EDZ7IPjVMJT<=*o`c=p4Ja0~dN+f`{cB|fER^e_jPc^@3gIYtKCuY*Ex zL&P+%&?<)5Jc_~i3{&YUDyx-yBsWE^BkM=#YKX?&^hg{vg|N%fz{WlS3bjoLfs&-{Dry1(8T6YAL|?3Qavm4 z%k$)tm&fZfD=esjC{H<)K%7Y%tqy;&4t{^52*TwQ@y*;KX3;%pJ0?Cj_BDfj>kH(ko>zA^PY^PTlkNf2;r% z8jx;ni(oQEs>s8TL!h%MJ&_t{^UWtLx8Utc<2$l}^Xo9I7XEuF_h8m(NB9Y&`hZ|< zJB*j3U)U&@k<#ds<6g2aO_Z|+4e=d`L2dDag70*$uO)lDoTOJq7JzjF@zEnb z(gY|A8k~j3|7K5`G%#Xn&=fP&UdCy>GTD(~3`2*kvk&9S%zD zA!R|E2vZU{koK)r{cW|}=9@srX_|PkKACiBxvA{Tlc5XI#sqMJgb9fWDfG$UU_DoB zr*eZoqN~Vl0V3?Q3!>N~tJi5lg*z4L(y;;@&L)d7)WP#R?}q^n20@%r#Pi&TIA^?P zlqXg38RF|hx%HkVzCOx>iM%2&?wE`q>J(xBk^CF7#Jg-qn-s*}TErM$kiymuH1&p? zPZbAHrx1OKVbGoB?LZfv9;S_=y+_>|^39j@3^pnYAE|bHdrWU>P?9Hx34z@}`_c>r zN(~;;G3x_7MxfxevG0jZsAsw7Tn;6wKdeM-gM`rL%exWua}5JJuTbHM{mgUrw(G~| z`|wnwL0=?+0GL2g`=^JgSTRK9FIe5#> z!$gbZmu463V-lk!@SLF=@#Qx`W7%LWNtit*qWxaDoccb4*ABoI{kcP%ljnc0{7{_H zw@$wOKlG6d{GJ1AIjUAfK(e+%suVt4J$L^nzO0A#wfTF`ed=vyKR?HhEaf}AAFKwZ zbLdgAm%sOwR8{=V?)3I6B0L1s18vLr<0zhgz(5x<^1F2`$|KAy1}M3L*ns^g8|5j< zSZ^=bj@89#_VaEn!UH-~-8}4V8bI6M^8S&Z^Hzx=NoJ2(pC?ikkO~@!N`2^Ofk!^V zJ9>cMT`b&zq6ZF^9VOO=?RLiL`>uJ1;Lc?Xz$_wS7M?JvW-m^E(p6XdwT?NMikS7 zJUdQPRu$9}wSp4Y7H|bYtaI|=|HgJ;VIxX{GGX_=?GTtjP>xfzF?iT$B7K-ywBm3> z>Tt0GeN>IA--%+$c9H_Q<-~$E?gkdk$R(z&H?HP{Hn9N?v68vK*?C{J@r523Ms7xR dyy)O<|NX*y|990PhySauefXibYnopA{{X51>(BrI literal 0 HcmV?d00001 diff --git a/README.md b/README.md index d25b597..b12af69 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,23 @@ -# SQLClean +

    + SQLClean +

    -Fast, opinionated SQL linting and deterministic formatting, with custom Rules in Python. +

    + Clean SQL, every time. Fast linting, safe fixes and one canonical format. +

    SQLClean checks `.sql` files against built-in Rules, applies their fixes and formats every file in one canonical layout. The engine and the `sqlclean` command are written in Rust. Custom Rules are plain Python functions that read structural facts about each statement. +

    + sqlclean check reports three findings, then sqlclean fix applies two fixes and formats the file +

    + +`sqlclean check` reports a bare `JOIN`, a `COUNT(1)` and a temporary table from a custom Rule. +`sqlclean fix` applies the two fixes and formats the file; the custom Rule's finding has no fix, +so it stays. + ## Install ```bash diff --git a/docs/recordings/.gitignore b/docs/recordings/.gitignore new file mode 100644 index 0000000..2082670 --- /dev/null +++ b/docs/recordings/.gitignore @@ -0,0 +1,2 @@ +scratch/ +out/ diff --git a/docs/recordings/README.md b/docs/recordings/README.md new file mode 100644 index 0000000..f0bc2c2 --- /dev/null +++ b/docs/recordings/README.md @@ -0,0 +1,31 @@ +# CLI recordings + +Terminal recordings of `sqlclean` for the README and the website, rendered with +[VHS](https://github.com/charmbracelet/vhs) from the `.tape` scripts in `tapes/`. The tapes and +scripts are the source; the rendered GIF, MP4 and PNG files go to `out/`, which is not committed. + +| Demo | Shows | +|------|-------| +| `check-fix` | `sqlclean check` reports built-in and custom findings, and `sqlclean fix` fixes and formats. | + +The README uses `check-fix`, copied into `.github/demos/`. + +## Requirements + +- `vhs`, `ttyd` and `ffmpeg` on `PATH`, plus a Chromium or Chrome that VHS can find. +- DejaVu Sans Mono, the font the tapes use. +- `sqlclean` on `PATH` with its Python package in the same environment, or `SQLCLEAN_BIN` pointing at + a directory that contains the `sqlclean` to record. + +## Record + +```bash +cd docs/recordings +./record.sh check-fix # out/check-fix.gif, out/check-fix.mp4 and out/check-fix.png +cp out/check-fix.gif ../../.github/demos/ +``` + +`record.sh` runs `prepare.sh ` to build a clean project under `scratch//`, then renders +`tapes/.tape`. Shared terminal settings and the colour theme are in `tapes/common.tape`. + +Look at every recording before publishing it. diff --git a/docs/recordings/prepare.sh b/docs/recordings/prepare.sh new file mode 100755 index 0000000..25eb94b --- /dev/null +++ b/docs/recordings/prepare.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# Build a clean scratch project for one demo. Usage: prepare.sh +set -euo pipefail +demo="$1" +here="$(cd "$(dirname "$0")" && pwd)" +dir="$here/scratch/$demo" +rm -rf "$dir" +mkdir -p "$dir" +cd "$dir" +case "$demo" in + check-fix) + # One messy query: a bare JOIN, COUNT(1) and a temporary table that a custom Rule forbids. + mkdir -p models sql_rules + cat > sqlclean.toml <<'TOML' +dialect = "duckdb" + +[rules] +select = ["SQC025", "XSQC"] +custom = ["sql_rules"] +TOML + cat > models/orders.sql <<'SQL' +select o.order_id, c.name, count(1) as items +from orders o join tmp_customers c on o.customer_id = c.customer_id +group by o.order_id, c.name +SQL + cat > sql_rules/style.py <<'PY' +from sqlclean import FixSafety, RuleContext, Statement, rule + + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list: + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith("tmp_") + ] + + +@rule( + code="XSQCSTYLE001", + message="COUNT(1) should be COUNT(*)", + remediation="Write COUNT(*).", + fixable=FixSafety.SAFE, + enabled_by_default=True, +) +def count_one(*, statement: Statement, ctx: RuleContext) -> list: + counts = {f.end + 1 for f in statement.functions if f.name.upper() == "COUNT"} + return [ + ctx.finding(subject=statement, node=literal, fix=[ctx.replace(node=literal, text="*")]) + for literal in statement.literals + if literal.text == "1" and literal.start in counts + ] +PY + ;; + *) + echo "unknown demo: $demo" >&2 + exit 2 ;; +esac diff --git a/docs/recordings/record.sh b/docs/recordings/record.sh new file mode 100755 index 0000000..38c4c9c --- /dev/null +++ b/docs/recordings/record.sh @@ -0,0 +1,14 @@ +#!/usr/bin/env bash +# Record one demo to out/.gif and out/.mp4. Usage: record.sh +# Uses the sqlclean on PATH; set SQLCLEAN_BIN to a directory containing another sqlclean to use that. +set -euo pipefail +demo="$1" +here="$(cd "$(dirname "$0")" && pwd)" +if [[ -n "${SQLCLEAN_BIN:-}" ]]; then + export PATH="$SQLCLEAN_BIN:$PATH" +fi +"$here/prepare.sh" "$demo" +mkdir -p "$here/out" +cd "$here" +# Headless Chromium's sandbox is unavailable in many containers and CI runners. +VHS_NO_SANDBOX="${VHS_NO_SANDBOX:-true}" vhs "tapes/$demo.tape" diff --git a/docs/recordings/tapes/check-fix.tape b/docs/recordings/tapes/check-fix.tape new file mode 100644 index 0000000..20610b8 --- /dev/null +++ b/docs/recordings/tapes/check-fix.tape @@ -0,0 +1,27 @@ +Source tapes/common.tape +Set Height 820 +Output out/check-fix.gif +Output out/check-fix.mp4 +Hide +Type "cd scratch/check-fix && export PS1='$ ' && clear" +Enter +Sleep 500ms +Show +Type "cat models/orders.sql" +Enter +Sleep 1500ms +Type "sqlclean check" +Sleep 300ms +Enter +Wait@60s /\$\s*$/ +Sleep 3s +Type "clear && sqlclean fix" +Sleep 300ms +Enter +Wait@60s /\$\s*$/ +Sleep 1500ms +Type "cat models/orders.sql" +Enter +Sleep 1s +Screenshot out/check-fix.png +Sleep 8s diff --git a/docs/recordings/tapes/common.tape b/docs/recordings/tapes/common.tape new file mode 100644 index 0000000..05d492f --- /dev/null +++ b/docs/recordings/tapes/common.tape @@ -0,0 +1,13 @@ +Set Shell bash +Set FontFamily "DejaVu Sans Mono" +Set FontSize 18 +Set LineHeight 1.0 +Set Width 1200 +Set Height 680 +Set Padding 28 +Set Margin 24 +Set MarginFill "#0d0f12" +Set BorderRadius 10 +Set WindowBar Colorful +Set TypingSpeed 45ms +Set Theme { "name": "SQLClean", "black": "#626975", "red": "#f47089", "green": "#54cf8e", "yellow": "#f0c267", "blue": "#3788e8", "magenta": "#a78bfa", "cyan": "#1fe0c0", "white": "#c9cdd4", "brightBlack": "#9ba1ac", "brightRed": "#f47089", "brightGreen": "#54cf8e", "brightYellow": "#f0c267", "brightBlue": "#4f98ee", "brightMagenta": "#c79bf0", "brightCyan": "#1fe0c0", "brightWhite": "#e9ebef", "background": "#111418", "foreground": "#c9cdd4", "selection": "#2a2f37", "cursor": "#e9ebef" } From 3e01d877747aa2dff4e0dc59e2a30705fa25add5 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 17:56:01 +0000 Subject: [PATCH 09/29] fix: count unsafe fixes apart in the check summary A finding whose only fix is unsafe was counted as fixable with plain sqlclean fix, which leaves it unchanged. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .../src/reporting/_helpers/text.rs | 34 +++++++++++++---- tests/e2e/src/sqlclean/hosting/_test_types.py | 10 +++++ .../sqlclean/hosting/test_sqlclean_command.py | 37 +++++++++++++++++++ {docs => website}/recordings/.gitignore | 0 {docs => website}/recordings/README.md | 0 {docs => website}/recordings/prepare.sh | 0 {docs => website}/recordings/record.sh | 0 .../recordings/tapes/check-fix.tape | 0 .../recordings/tapes/common.tape | 0 9 files changed, 74 insertions(+), 7 deletions(-) rename {docs => website}/recordings/.gitignore (100%) rename {docs => website}/recordings/README.md (100%) rename {docs => website}/recordings/prepare.sh (100%) rename {docs => website}/recordings/record.sh (100%) rename {docs => website}/recordings/tapes/check-fix.tape (100%) rename {docs => website}/recordings/tapes/common.tape (100%) diff --git a/crates/sqlclean-cli/src/reporting/_helpers/text.rs b/crates/sqlclean-cli/src/reporting/_helpers/text.rs index cf96bc2..3e697f9 100644 --- a/crates/sqlclean-cli/src/reporting/_helpers/text.rs +++ b/crates/sqlclean-cli/src/reporting/_helpers/text.rs @@ -3,7 +3,7 @@ use std::collections::HashMap; use std::fmt::Write; -use crate::checking::models::CheckOutcome; +use crate::checking::models::{CheckOutcome, FixSafety}; use crate::config::constants::CONFIG_FILE_NAME; use crate::reporting::models::LocatedFinding; @@ -28,24 +28,44 @@ pub(crate) fn render( stale.rule, stale.path ); } - let fixable: usize = located - .iter() - .filter(|finding| finding.fix.is_some()) - .count(); + let safe: usize = fix_count(located, FixSafety::Safe); + let unsafe_only: usize = fix_count(located, FixSafety::Unsafe); let problems: usize = located.len() + outcome.stale_exceptions.len(); if problems == 0 { report.push_str("All checks passed.\n"); } else { let noun: &str = if problems == 1 { "problem" } else { "problems" }; let _ = write!(report, "Found {problems} {noun}"); - if fixable > 0 { - let _ = write!(report, " ({fixable} fixable with `sqlclean fix`)"); + match (safe, unsafe_only) { + (0, 0) => {} + (safe, 0) => { + let _ = write!(report, " ({safe} fixable with `sqlclean fix`)"); + } + (0, unsafe_only) => { + let _ = write!( + report, + " ({unsafe_only} fixable with `sqlclean fix --unsafe-fixes`)" + ); + } + (safe, unsafe_only) => { + let _ = write!( + report, + " ({safe} fixable with `sqlclean fix`, {unsafe_only} more with `--unsafe-fixes`)" + ); + } } report.push_str(".\n"); } report } +fn fix_count(located: &[LocatedFinding], safety: FixSafety) -> usize { + located + .iter() + .filter(|finding| finding.fix == Some(safety)) + .count() +} + fn finding_text(finding: &LocatedFinding, text: Option<&String>) -> String { let mut report = String::new(); let _ = writeln!(report, "error[{}]: {}", finding.code, finding.message); diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py index b6dbbb6..205a29b 100644 --- a/tests/e2e/src/sqlclean/hosting/_test_types.py +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -49,3 +49,13 @@ class CacheTestCase: expected_exits: tuple[int, int, int] expected_cached_code: str expected_error: str + + +@dataclass(frozen=True) +class SummaryTestCase: + """SQL whose text report must end with one summary line.""" + + description: str + config: str + source: str + expected_summary: str diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 891a654..c418a42 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -13,6 +13,7 @@ CheckTestCase, IdempotencyTestCase, RewriteTestCase, + SummaryTestCase, ) from tests.e2e.src.sqlclean.hosting.helpers import run, write_project from tests.integration.src.sqlclean.rule_testing.helpers import SAMPLE_RULES @@ -182,6 +183,42 @@ def test_given_project_when_rewriting_then_files_and_exit_status_match( assert files == test_case.expected_files +@pytest.mark.parametrize( + "test_case", + [ + SummaryTestCase( + description="a safe fix is fixable with plain fix", + config=DUCKDB + '[rules]\nselect = ["SQC002"]\n', + source="select a.id from a, b\n", + expected_summary="Found 1 problem (1 fixable with `sqlclean fix`).", + ), + SummaryTestCase( + description="an unsafe fix needs --unsafe-fixes", + config=DUCKDB + '[rules]\nselect = ["SQC001"]\n', + source="select a.id from a where a.x = null\n", + expected_summary="Found 1 problem (1 fixable with `sqlclean fix --unsafe-fixes`).", + ), + SummaryTestCase( + description="safe and unsafe fixes are counted apart", + config=DUCKDB + '[rules]\nselect = ["SQC001", "SQC002"]\n', + source=NULL_AND_COMMA, + expected_summary=( + "Found 2 problems (1 fixable with `sqlclean fix`, 1 more with `--unsafe-fixes`)." + ), + ), + ], + ids=lambda case: case.description, +) +def test_given_fixable_findings_when_checking_then_summary_counts_safe_and_unsafe_fixes( + tmp_path: Path, test_case: SummaryTestCase +) -> None: + write_project(tmp_path, config=test_case.config, files={"q.sql": test_case.source}) + + completed: subprocess.CompletedProcess[str] = run(tmp_path, "check", environment={}) + + assert completed.stdout.rstrip().splitlines()[-1] == test_case.expected_summary + + @pytest.mark.parametrize( "test_case", [ diff --git a/docs/recordings/.gitignore b/website/recordings/.gitignore similarity index 100% rename from docs/recordings/.gitignore rename to website/recordings/.gitignore diff --git a/docs/recordings/README.md b/website/recordings/README.md similarity index 100% rename from docs/recordings/README.md rename to website/recordings/README.md diff --git a/docs/recordings/prepare.sh b/website/recordings/prepare.sh similarity index 100% rename from docs/recordings/prepare.sh rename to website/recordings/prepare.sh diff --git a/docs/recordings/record.sh b/website/recordings/record.sh similarity index 100% rename from docs/recordings/record.sh rename to website/recordings/record.sh diff --git a/docs/recordings/tapes/check-fix.tape b/website/recordings/tapes/check-fix.tape similarity index 100% rename from docs/recordings/tapes/check-fix.tape rename to website/recordings/tapes/check-fix.tape diff --git a/docs/recordings/tapes/common.tape b/website/recordings/tapes/common.tape similarity index 100% rename from docs/recordings/tapes/common.tape rename to website/recordings/tapes/common.tape From 179e62e3eba707e0085a5b11cc1b797a6afe1229 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 17:57:06 +0000 Subject: [PATCH 10/29] fix: locate qualified tables from their first name part A table fact for raw.payments covered only payments, so findings and fixes at the node left the schema out. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .../src/sql_facts/_helpers/collection.rs | 8 +++++- .../src/sql_facts/tests/test_file_facts.rs | 26 ++++++++++++------- .../src/sql_facts/tests/test_types.rs | 2 +- 3 files changed, 24 insertions(+), 12 deletions(-) diff --git a/crates/sqlclean/src/sql_facts/_helpers/collection.rs b/crates/sqlclean/src/sql_facts/_helpers/collection.rs index ad5efc3..5fd0bbd 100644 --- a/crates/sqlclean/src/sql_facts/_helpers/collection.rs +++ b/crates/sqlclean/src/sql_facts/_helpers/collection.rs @@ -155,6 +155,12 @@ fn table_node(node: &Expression) -> Option { return None; }; let span: Span = table.span.or(table.name.span)?; + let start: usize = [&table.catalog, &table.schema] + .into_iter() + .flatten() + .filter_map(|part| part.span) + .map(|part| part.start) + .fold(span.start, usize::min); let qualified_name: String = [&table.catalog, &table.schema] .into_iter() .flatten() @@ -163,7 +169,7 @@ fn table_node(node: &Expression) -> Option { .collect::>() .join("."); Some(TableNode { - start: span.start, + start, end: span.end, name: table.name.name.clone(), qualified_name, diff --git a/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs b/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs index 0a275c9..7e60040 100644 --- a/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs +++ b/crates/sqlclean/src/sql_facts/tests/test_file_facts.rs @@ -22,7 +22,11 @@ SELECT r.order_id, UPPER(c.name) AS name, 'é' AS mark\n\ FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", expected_terminator: ";", expected_ctes: &["recent"], - expected_tables: &[("shop.orders", "o"), ("recent", "r"), ("customers", "c")], + expected_tables: &[ + ("shop.orders", "o", "shop.orders"), + ("recent", "r", "recent"), + ("customers", "c", "customers"), + ], expected_joins: &[( "LEFT", "customers", @@ -42,7 +46,7 @@ FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", expected_text: "SELECT COUNT(*) FROM customers", expected_terminator: "", expected_ctes: &[], - expected_tables: &[("customers", "")], + expected_tables: &[("customers", "", "customers")], expected_joins: &[], expected_stars: &[], expected_functions: &["COUNT"], @@ -57,7 +61,7 @@ FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", expected_text: "SELECT a.id FROM a CROSS JOIN b", expected_terminator: "", expected_ctes: &[], - expected_tables: &[("a", ""), ("b", "")], + expected_tables: &[("a", "", "a"), ("b", "", "b")], expected_joins: &[("CROSS", "b", false, "CROSS JOIN b")], expected_stars: &[], expected_functions: &[], @@ -72,7 +76,7 @@ FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", expected_text: "SELECT a.id FROM a INNER JOIN (SELECT id FROM b) AS s ON a.id = s.id LEFT JOIN c USING (id) WHERE LEFT(a.name, 1) = 'x'", expected_terminator: "", expected_ctes: &[], - expected_tables: &[("a", ""), ("b", ""), ("c", "")], + expected_tables: &[("a", "", "a"), ("b", "", "b"), ("c", "", "c")], expected_joins: &[ ( "INNER", @@ -115,21 +119,23 @@ FROM recent AS r LEFT JOIN customers AS c ON r.customer_id = c.customer_id", .map(|cte| located(cte.start, cte.end)) .collect(); assert_eq!(ctes, test_case.expected_ctes, "{}", test_case.description); - let tables: Vec<(&str, &str)> = statement + let tables: Vec<(&str, &str, String)> = statement .tables .iter() .map(|table| { ( table.qualified_name.as_str(), table.alias.as_deref().unwrap_or(""), + located(table.start, table.end), ) }) .collect(); - assert_eq!( - tables, test_case.expected_tables, - "{}", - test_case.description - ); + let expected_tables: Vec<(&str, &str, String)> = test_case + .expected_tables + .iter() + .map(|(name, alias, text)| (*name, *alias, (*text).to_owned())) + .collect(); + assert_eq!(tables, expected_tables, "{}", test_case.description); let joins: Vec<(&str, &str, bool, String)> = statement .joins .iter() diff --git a/crates/sqlclean/src/sql_facts/tests/test_types.rs b/crates/sqlclean/src/sql_facts/tests/test_types.rs index 3b3ac78..e6329e4 100644 --- a/crates/sqlclean/src/sql_facts/tests/test_types.rs +++ b/crates/sqlclean/src/sql_facts/tests/test_types.rs @@ -6,7 +6,7 @@ pub(crate) struct StatementFactsTestCase { pub(crate) expected_text: &'static str, pub(crate) expected_terminator: &'static str, pub(crate) expected_ctes: &'static [&'static str], - pub(crate) expected_tables: &'static [(&'static str, &'static str)], + pub(crate) expected_tables: &'static [(&'static str, &'static str, &'static str)], pub(crate) expected_joins: &'static [(&'static str, &'static str, bool, &'static str)], pub(crate) expected_stars: &'static [&'static str], pub(crate) expected_functions: &'static [&'static str], From 1f322070f529f6415e56690786036157c404abc2 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 17:58:25 +0000 Subject: [PATCH 11/29] fix: separate the message and remediation in SARIF results Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .../src/reporting/_helpers/machine.rs | 2 +- tests/e2e/src/sqlclean/hosting/_test_types.py | 10 +++++++ .../sqlclean/hosting/test_sqlclean_command.py | 26 +++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) diff --git a/crates/sqlclean-cli/src/reporting/_helpers/machine.rs b/crates/sqlclean-cli/src/reporting/_helpers/machine.rs index e071fd7..7753ca0 100644 --- a/crates/sqlclean-cli/src/reporting/_helpers/machine.rs +++ b/crates/sqlclean-cli/src/reporting/_helpers/machine.rs @@ -36,7 +36,7 @@ pub(crate) fn render_sarif(located: &[LocatedFinding]) -> String { json!({ "ruleId": finding.code, "level": "error", - "message": { "text": format!("{} {}", finding.message, finding.remediation) }, + "message": { "text": format!("{}. {}", finding.message.trim_end_matches('.'), finding.remediation) }, "locations": [{ "physicalLocation": { "artifactLocation": { "uri": finding.path }, diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py index 205a29b..aa1face 100644 --- a/tests/e2e/src/sqlclean/hosting/_test_types.py +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -59,3 +59,13 @@ class SummaryTestCase: config: str source: str expected_summary: str + + +@dataclass(frozen=True) +class SarifTestCase: + """A project whose SARIF report must carry one result message.""" + + description: str + config: str + source: str + expected_message: str diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index c418a42..9a02af0 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -13,6 +13,7 @@ CheckTestCase, IdempotencyTestCase, RewriteTestCase, + SarifTestCase, SummaryTestCase, ) from tests.e2e.src.sqlclean.hosting.helpers import run, write_project @@ -219,6 +220,31 @@ def test_given_fixable_findings_when_checking_then_summary_counts_safe_and_unsaf assert completed.stdout.rstrip().splitlines()[-1] == test_case.expected_summary +@pytest.mark.parametrize( + "test_case", + [ + SarifTestCase( + description="the message and remediation read as two sentences", + config=DUCKDB + '[rules]\nselect = ["SQC025"]\n', + source="select a.id from a join b on a.id = b.id\n", + expected_message="JOIN type is implicit. Write INNER JOIN explicitly.", + ), + ], + ids=lambda case: case.description, +) +def test_given_findings_when_reporting_sarif_then_each_result_has_a_readable_message( + tmp_path: Path, test_case: SarifTestCase +) -> None: + write_project(tmp_path, config=test_case.config, files={"q.sql": test_case.source}) + + completed: subprocess.CompletedProcess[str] = run( + tmp_path, "check", "--output-format", "sarif", environment={} + ) + results: list[dict[str, dict[str, str]]] = json.loads(completed.stdout)["runs"][0]["results"] + + assert [result["message"]["text"] for result in results] == [test_case.expected_message] + + @pytest.mark.parametrize( "test_case", [ From 38564c54cb25dad0f72ded1c2c245ee89fc10d7b Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 18:54:58 +0000 Subject: [PATCH 12/29] perf(cli): format files in parallel sqlclean format formatted one file at a time while check already used rayon. On 10,282 files format --check drops from about 10 s to 2.7 s on 4 CPUs; output keeps file order. Co-authored-by: Kevin Longe --- .../src/command/_helpers/format_command.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/sqlclean-cli/src/command/_helpers/format_command.rs b/crates/sqlclean-cli/src/command/_helpers/format_command.rs index 5ce2aca..d69251b 100644 --- a/crates/sqlclean-cli/src/command/_helpers/format_command.rs +++ b/crates/sqlclean-cli/src/command/_helpers/format_command.rs @@ -2,6 +2,8 @@ use std::io::Write; +use rayon::iter::{IntoParallelRefIterator, ParallelIterator}; + use crate::command::_helpers::diffs::{print_diff, write_file}; use crate::command::constants::{EXIT_FINDINGS, EXIT_SUCCESS}; use crate::command::models::FormatArgs; @@ -13,8 +15,12 @@ pub(crate) fn run(config: &Config, arguments: &FormatArgs) -> Result let files = discover_files(config, &arguments.paths)?; let mut stderr = std::io::stderr(); let (mut changed, mut refused): (usize, usize) = (0, 0); - for file in &files { - let formatted: String = match format_source(&file.text, &file.dialect, config.line_width) { + let results: Vec> = files + .par_iter() + .map(|file| format_source(&file.text, &file.dialect, config.line_width)) + .collect(); + for (file, result) in files.iter().zip(results) { + let formatted: String = match result { Ok(formatted) => formatted, Err(reason) => { refused += 1; From 12ce1bbe14285f15766c6fa9894681373f490d90 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 18:55:13 +0000 Subject: [PATCH 13/29] feat(format): indent four spaces and break only overflowing groups - Formatted SQL is indented four spaces per step by default, matching SQLFluff and sqruff; [format] indent chooses 1 to 8. The engine keeps two spaces unless a request passes indent, so embedding tools keep their current layout. - The parse-tree layout broke every parenthesis level of a long expression. Groups that fit on their line are now rejoined, innermost first, so only the levels that overflow break. A group that fits once its leading operator starts the line breaks before that operator. - A leading comma after a line comment moves to the end of the commented line instead of standing on its own line. - A file that holds only comments keeps its final newline. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .../src/command/_helpers/format_command.rs | 2 +- .../src/config/_helpers/file_schema.rs | 1 + .../src/config/_helpers/loading.rs | 3 +- .../src/config/_helpers/validation.rs | 6 + crates/sqlclean-cli/src/config/constants.rs | 2 + crates/sqlclean-cli/src/config/models.rs | 2 + .../src/config/tests/test_load_config.rs | 5 + .../src/fixing/_helpers/passes.rs | 2 +- .../src/formatting/main/format_source.rs | 24 ++- .../src/sql_lint/_helpers/formatter.rs | 11 +- .../src/sql_lint/_helpers/printed_layout.rs | 175 +++++++++++++++++- .../src/sql_lint/_helpers/token_layout.rs | 27 ++- crates/sqlclean/src/sql_lint/models.rs | 3 + .../src/sql_lint/tests/test_layout_rules.rs | 7 + .../src/sql_lint/tests/test_line_wrap.rs | 14 ++ .../sqlclean/hosting/test_sqlclean_command.py | 28 +++ 16 files changed, 293 insertions(+), 19 deletions(-) diff --git a/crates/sqlclean-cli/src/command/_helpers/format_command.rs b/crates/sqlclean-cli/src/command/_helpers/format_command.rs index d69251b..37cfe72 100644 --- a/crates/sqlclean-cli/src/command/_helpers/format_command.rs +++ b/crates/sqlclean-cli/src/command/_helpers/format_command.rs @@ -17,7 +17,7 @@ pub(crate) fn run(config: &Config, arguments: &FormatArgs) -> Result let (mut changed, mut refused): (usize, usize) = (0, 0); let results: Vec> = files .par_iter() - .map(|file| format_source(&file.text, &file.dialect, config.line_width)) + .map(|file| format_source(&file.text, &file.dialect, config.line_width, config.indent)) .collect(); for (file, result) in files.iter().zip(results) { let formatted: String = match result { diff --git a/crates/sqlclean-cli/src/config/_helpers/file_schema.rs b/crates/sqlclean-cli/src/config/_helpers/file_schema.rs index 383c875..e4607d2 100644 --- a/crates/sqlclean-cli/src/config/_helpers/file_schema.rs +++ b/crates/sqlclean-cli/src/config/_helpers/file_schema.rs @@ -25,6 +25,7 @@ pub(crate) struct ConfigFile { #[serde(deny_unknown_fields)] pub(crate) struct FormatTable { pub(crate) line_width: Option, + pub(crate) indent: Option, } #[derive(Debug, Default, Deserialize)] diff --git a/crates/sqlclean-cli/src/config/_helpers/loading.rs b/crates/sqlclean-cli/src/config/_helpers/loading.rs index a3624a5..b3c3100 100644 --- a/crates/sqlclean-cli/src/config/_helpers/loading.rs +++ b/crates/sqlclean-cli/src/config/_helpers/loading.rs @@ -6,7 +6,7 @@ use std::path::{Path, PathBuf}; use crate::config::_helpers::file_schema::ConfigFile; use crate::config::_helpers::validation::validate; use crate::config::constants::{ - CONFIG_FILE_NAME, DEFAULT_DIALECT, DEFAULT_INCLUDE, DEFAULT_LINE_WIDTH, + CONFIG_FILE_NAME, DEFAULT_DIALECT, DEFAULT_INCLUDE, DEFAULT_INDENT, DEFAULT_LINE_WIDTH, }; use crate::config::models::{Config, RulesConfig}; @@ -51,6 +51,7 @@ fn effective_config(root: PathBuf, file: ConfigFile) -> Config { exclude: file.exclude, dialects: file.dialects, line_width: file.format.line_width.unwrap_or(DEFAULT_LINE_WIDTH), + indent: file.format.indent.unwrap_or(DEFAULT_INDENT), rules: RulesConfig { select: file.rules.select, ignore: file.rules.ignore, diff --git a/crates/sqlclean-cli/src/config/_helpers/validation.rs b/crates/sqlclean-cli/src/config/_helpers/validation.rs index 7a72768..190a703 100644 --- a/crates/sqlclean-cli/src/config/_helpers/validation.rs +++ b/crates/sqlclean-cli/src/config/_helpers/validation.rs @@ -3,6 +3,7 @@ use globset::Glob; use sqlclean::sql_lint::main::validate_dialect::validate_dialect; +use crate::config::constants::MAX_INDENT; use crate::config::models::Config; pub(crate) fn validate(config: &Config) -> Result<(), String> { @@ -16,6 +17,11 @@ pub(crate) fn validate(config: &Config) -> Result<(), String> { if config.line_width == 0 { return Err("[format] line_width must be positive".to_owned()); } + if !(1..=MAX_INDENT).contains(&config.indent) { + return Err(format!( + "[format] indent must be between 1 and {MAX_INDENT}" + )); + } let thresholds = config.rules.thresholds; if thresholds.max_literal_length == 0 || thresholds.max_ranking_order_by == 0 { return Err( diff --git a/crates/sqlclean-cli/src/config/constants.rs b/crates/sqlclean-cli/src/config/constants.rs index 692df56..2d6a743 100644 --- a/crates/sqlclean-cli/src/config/constants.rs +++ b/crates/sqlclean-cli/src/config/constants.rs @@ -2,5 +2,7 @@ pub const CONFIG_FILE_NAME: &str = "sqlclean.toml"; pub(crate) const DEFAULT_DIALECT: &str = "generic"; pub(crate) const DEFAULT_INCLUDE: &str = "**/*.sql"; pub(crate) const DEFAULT_LINE_WIDTH: usize = 100; +pub(crate) const DEFAULT_INDENT: usize = 4; +pub(crate) const MAX_INDENT: usize = 8; pub(crate) const DEFAULT_MAX_LITERAL_LENGTH: usize = 100; pub(crate) const DEFAULT_MAX_RANKING_ORDER_BY: usize = 5; diff --git a/crates/sqlclean-cli/src/config/models.rs b/crates/sqlclean-cli/src/config/models.rs index e44b772..0db827f 100644 --- a/crates/sqlclean-cli/src/config/models.rs +++ b/crates/sqlclean-cli/src/config/models.rs @@ -15,6 +15,8 @@ pub struct Config { pub exclude: Vec, pub dialects: Vec, pub line_width: usize, + /// Spaces per indent step in formatted SQL. + pub indent: usize, pub rules: RulesConfig, } diff --git a/crates/sqlclean-cli/src/config/tests/test_load_config.rs b/crates/sqlclean-cli/src/config/tests/test_load_config.rs index 7fdb954..021acd3 100644 --- a/crates/sqlclean-cli/src/config/tests/test_load_config.rs +++ b/crates/sqlclean-cli/src/config/tests/test_load_config.rs @@ -97,6 +97,11 @@ fn given_invalid_values_when_loading_then_each_is_rejected_with_its_reason() -> config_text: "[format]\nline_width = 0\n", expected_error: "line_width must be positive", }, + RejectedConfigTestCase { + description: "an indent of zero is rejected", + config_text: "[format]\nindent = 0\n", + expected_error: "indent must be between 1 and 8", + }, ]; for test_case in &test_cases { let directory = tempfile::tempdir().map_err(|error| error.to_string())?; diff --git a/crates/sqlclean-cli/src/fixing/_helpers/passes.rs b/crates/sqlclean-cli/src/fixing/_helpers/passes.rs index 9efc78e..4768da3 100644 --- a/crates/sqlclean-cli/src/fixing/_helpers/passes.rs +++ b/crates/sqlclean-cli/src/fixing/_helpers/passes.rs @@ -35,7 +35,7 @@ pub(crate) fn fix_sources( } let mut format_refusals: Vec<(String, String)> = Vec::new(); for file in &mut files { - match format_source(&file.text, &file.dialect, config.line_width) { + match format_source(&file.text, &file.dialect, config.line_width, config.indent) { Ok(formatted) => file.text = formatted, Err(reason) => format_refusals.push((file.path.clone(), reason)), } diff --git a/crates/sqlclean-cli/src/formatting/main/format_source.rs b/crates/sqlclean-cli/src/formatting/main/format_source.rs index 52bb41b..0f5ebb8 100644 --- a/crates/sqlclean-cli/src/formatting/main/format_source.rs +++ b/crates/sqlclean-cli/src/formatting/main/format_source.rs @@ -8,7 +8,12 @@ use sqlclean::sql_lint::models::{FormatRequest, FormatResponse}; use crate::formatting::constants::{FORMAT_REFUSAL as REFUSAL, STATEMENT_SEPARATOR}; /// `text` in canonical layout ending in one newline, or why the formatter left it unchanged. -pub fn format_source(text: &str, dialect: &str, line_width: usize) -> Result { +pub fn format_source( + text: &str, + dialect: &str, + line_width: usize, + indent: usize, +) -> Result { let facts = file_facts(text, dialect)?; let characters: Vec = text.chars().collect(); let mut boundaries: Vec = facts @@ -24,23 +29,34 @@ pub fn format_source(text: &str, dialect: &str, line_width: usize) -> Result Result { +fn format_statement( + sql: &str, + dialect: &str, + line_width: usize, + indent: usize, +) -> Result { let request = FormatRequest { version: LINT_API_VERSION, sql: sql.to_owned(), dialect: dialect.to_owned(), line_width: Some(line_width), + indent: Some(indent), ..FormatRequest::default() }; let response: FormatResponse = format_requests(vec![request]) diff --git a/crates/sqlclean/src/sql_lint/_helpers/formatter.rs b/crates/sqlclean/src/sql_lint/_helpers/formatter.rs index c162402..1583848 100644 --- a/crates/sqlclean/src/sql_lint/_helpers/formatter.rs +++ b/crates/sqlclean/src/sql_lint/_helpers/formatter.rs @@ -23,6 +23,8 @@ thread_local! { pub(crate) static RELAYOUTS: std::cell::Cell = const { std::cell::Cell::new(0) }; } +/// Spaces per indent step when a request does not choose. +const DEFAULT_INDENT: usize = 2; const NOT_IDEMPOTENT_FAILURE: &str = "native formatter output is not idempotent"; const UNSUPPORTED_SQL_FAILURE: &str = "native formatter could not safely restore literal or cast spelling"; @@ -89,6 +91,7 @@ fn try_format_sql(request: FormatRequest) -> Result { let layout = Layout { dialect: &dialect, wrap: wrap.as_ref(), + indent: request.indent.unwrap_or(DEFAULT_INDENT), }; let (formatted, formatted_tokens, oracle) = format_layout(&original, &layout, &parse_options)?; let changed = formatted != original; @@ -171,10 +174,11 @@ fn relayout( Ok(layout.print(&authored, oracle)?.0) } -/// The dialect and optional line wrapping every printed layout uses. +/// The dialect, indent and optional line wrapping every printed layout uses. struct Layout<'a> { dialect: &'a Dialect, wrap: Option<&'a WrapOptions<'a>>, + indent: usize, } impl Layout<'_> { @@ -189,6 +193,11 @@ impl Layout<'_> { let Some(mut sql) = PrintedSql::parse(&printed, self.dialect, token_widths)? else { return Ok((printed, recases, texts)); }; + sql.set_indent(self.indent); + sql.attach_commas_before_comments(); + if let Some(wrap) = self.wrap { + sql.join_fitting_groups(wrap.line_width); + } sql.arrange_clauses(self.wrap.map_or(usize::MAX, |wrap| wrap.line_width)); if let Some(wrap) = self.wrap { wrap_lines(&mut sql, wrap.line_width); diff --git a/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs b/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs index 5aa8522..a57a34e 100644 --- a/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs +++ b/crates/sqlclean/src/sql_lint/_helpers/printed_layout.rs @@ -7,7 +7,8 @@ use polyglot_sql::tokens::TokenType; use crate::sql_lint::constants::OPEN_PARENTHESIS; -const CONTINUATION_INDENT: usize = 2; +/// The indent step of the parse-tree layout every printed statement starts from. +const PARSED_INDENT: usize = 2; const COMMA: &str = ","; const SINGLE_SPACE: &str = " "; const BETWEEN_KEYWORD: &str = "BETWEEN"; @@ -73,6 +74,8 @@ pub(crate) struct PrintedSql<'a> { /// Gaps that line wrapping must keep: inside VALUES rows and type parameter lists. locked: Vec, token_widths: &'a HashMap, + /// Spaces per indent step. + indent: usize, } /// One printed line; end columns are absent when comments or multi-line tokens pin it. @@ -119,6 +122,7 @@ impl<'a> PrintedSql<'a> { partners, locked, token_widths, + indent: PARSED_INDENT, })) } @@ -252,6 +256,157 @@ fn newline(indentation: usize) -> String { } impl PrintedSql<'_> { + /// Re-indent every line from the parse-tree indent step to `indent` spaces per step. + pub(crate) fn set_indent(&mut self, indent: usize) { + if indent == self.indent { + return; + } + let step = self.indent; + let scale = |spaces: usize| spaces / step * indent + spaces % step; + for gap in &mut self.gaps { + if !gap.contains('\n') { + continue; + } + let comment_start = if gap.contains("/*") { + gap.rfind('\n').unwrap_or(0) + } else { + 0 + }; + let (kept, scaled) = gap.split_at(comment_start); + let mut rebuilt = kept.to_string(); + for (position, segment) in scaled.split('\n').enumerate() { + if position > 0 { + rebuilt.push('\n'); + let spaces = segment.len() - segment.trim_start_matches(' ').len(); + rebuilt.push_str(&" ".repeat(scale(spaces))); + rebuilt.push_str(&segment[spaces..]); + } else { + rebuilt.push_str(segment); + } + } + *gap = rebuilt; + } + self.indent = indent; + } + + /// Rejoin each multi-line expression group, innermost first, whose joined line fits. + pub(crate) fn join_fitting_groups(&mut self, line_width: usize) { + let mut openers: Vec = (0..self.len()) + .filter(|&index| self.tokens[index] == OPEN_PARENTHESIS) + .collect(); + openers.sort_by_key(|&index| std::cmp::Reverse(self.depths[index])); + for opener in openers { + let Some(closer) = self.partners[opener] else { + continue; + }; + if !self.spans_lines(opener, closer) || !self.joinable_group(opener, closer) { + continue; + } + let (first, _) = self.line_of(opener); + let (_, last) = self.line_of(closer); + let indentation = self.indentation(first); + let fits = |start: usize| { + self.joined_width(start, last, indentation) + .is_some_and(|width| width <= line_width) + }; + let start = if fits(first) { + first + } else if let Some(operator) = self.leading_operator_before(first, opener) + && fits(operator) + { + operator + } else { + continue; + }; + for index in opener + 1..=closer { + if self.gaps[index].contains('\n') { + self.gaps[index] = self.joined_gap(index).to_string(); + } + } + if start != first { + self.gaps[start] = newline(indentation); + } + } + } + + /// The last binary operator on the line from `first` that applies to the group at `opener`. + fn leading_operator_before(&self, first: usize, opener: usize) -> Option { + (first + 1..opener).rev().find(|&index| { + self.depths[index] == self.depths[opener] + && LEADING_OPERATORS.contains(&self.upper(index).as_str()) + && self.gaps[index] == SINGLE_SPACE + && self.ends_operand(index - 1) + }) + } + + /// Move a comma that follows a line comment to the end of the commented line. + pub(crate) fn attach_commas_before_comments(&mut self) { + for index in 1..self.len().saturating_sub(1) { + if self.tokens[index] != COMMA { + continue; + } + let Some((comment, rest)) = self.gaps[index].split_once('\n') else { + continue; + }; + if !comment.trim_start().starts_with("--") || !rest.trim().is_empty() { + continue; + } + let (comment, rest) = (comment.to_string(), rest.to_string()); + let next = self.gaps[index + 1].trim_start_matches(' ').to_string(); + self.gaps[index] = String::new(); + self.gaps[index + 1] = if next.starts_with('\n') { + comment + &next + } else { + format!("{comment}\n{rest}{next}") + }; + } + } + + fn joinable_group(&self, opener: usize, closer: usize) -> bool { + (opener + 1..closer).all(|index| { + !(self.kinds[index] == TokenType::Select + || self.is(index, VALUES_KEYWORD) + || (self.is(index, CASE_KEYWORD) + && self + .case_end(index) + .is_none_or(|end| self.spans_lines(index, end)))) + }) + } + + /// The width of tokens `first..=last` on one line at `indentation`; `None` when comments pin. + fn joined_width(&self, first: usize, last: usize, indentation: usize) -> Option { + let trailing = self.gaps[last + 1].split('\n').next().unwrap_or_default(); + let pinned = !trailing.trim().is_empty() + || (first + 1..=last).any(|index| !self.gaps[index].trim().is_empty()) + || (first..=last).any(|index| self.tokens[index].contains('\n')); + if pinned { + return None; + } + let mut width = indentation; + for index in first..=last { + if index > first { + width += if self.gaps[index].contains('\n') { + self.joined_gap(index).len() + } else { + self.gaps[index].chars().count() + }; + } + width += self.width(index); + } + Some(width) + } + + /// The gap before `index` once its line break is removed. + fn joined_gap(&self, index: usize) -> &'static str { + if self.tokens[index - 1] == OPEN_PARENTHESIS + || matches!(self.tokens[index].as_str(), ")" | ",") + { + "" + } else { + SINGLE_SPACE + } + } + /// Apply the layout rules that only move existing line breaks before wrapping. pub(crate) fn arrange_clauses(&mut self, line_width: usize) { for index in 0..self.len() { @@ -368,11 +523,11 @@ impl PrintedSql<'_> { && self.tokens[values - 1] == OPEN_PARENTHESIS && let Some(closer) = self.partners[values - 1] { - self.set_break(values, &newline(indentation + CONTINUATION_INDENT)); + self.set_break(values, &newline(indentation + self.indent)); self.set_break(closer, &newline(indentation)); - indentation += CONTINUATION_INDENT; + indentation += self.indent; } - let row_line = newline(indentation + CONTINUATION_INDENT); + let row_line = newline(indentation + self.indent); for (opener, _) in rows { self.set_break(opener, &row_line); } @@ -684,7 +839,7 @@ impl PrintedSql<'_> { let values_row = self.tokens[line.first] == OPEN_PARENTHESIS && self.locked[line.first + 1]; !values_row && (self.break_before(line, &CHAIN_KEYWORDS, 0) - || self.break_before(line, &ARITHMETIC_OPERATORS, CONTINUATION_INDENT) + || self.break_before(line, &ARITHMETIC_OPERATORS, self.indent) || self.break_values_rows(line) || self.explode_group(line) || self.break_after_commas(line)) @@ -708,7 +863,7 @@ impl PrintedSql<'_> { { return false; } - let row_line = newline(line.indentation + CONTINUATION_INDENT); + let row_line = newline(line.indentation + self.indent); for (opener, _) in rows { self.gaps[opener] = row_line.clone(); } @@ -751,9 +906,9 @@ impl PrintedSql<'_> { return false; }; if closer > line.last { - self.indent_lines(opener + 1, closer - 1, CONTINUATION_INDENT); + self.indent_lines(opener + 1, closer - 1, self.indent); } - let inner = newline(line.indentation + CONTINUATION_INDENT); + let inner = newline(line.indentation + self.indent); if self.is(opener, CASE_KEYWORD) { self.explode_case(opener, closer, line.indentation); return true; @@ -823,7 +978,7 @@ impl PrintedSql<'_> { /// Put each WHEN, THEN and ELSE of a one-line CASE on its own line and END under CASE. fn explode_case(&mut self, case: usize, end: usize, indentation: usize) { - let inner = newline(indentation + CONTINUATION_INDENT); + let inner = newline(indentation + self.indent); let depth = self.depths[case]; let mut nested = 0_usize; for index in case + 1..end { @@ -900,7 +1055,7 @@ impl PrintedSql<'_> { else { return false; }; - let indentation = newline(line.indentation + CONTINUATION_INDENT); + let indentation = newline(line.indentation + self.indent); let mut changed = false; for index in line.first..line.last { if self.depths[index] == depth && self.tokens[index] == COMMA && !self.locked[index + 1] diff --git a/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs b/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs index 8b5a43a..17ffd1d 100644 --- a/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs +++ b/crates/sqlclean/src/sql_lint/_helpers/token_layout.rs @@ -343,7 +343,12 @@ pub(crate) fn verify_printed_tokens( return Err(TOKEN_PRESERVATION_FAILURE.to_string()); } } - if comment_positions(before, before_tokens) != comment_positions(after, after_tokens) { + let attached = comments_stay_attached( + &comment_positions(before, before_tokens), + &comment_positions(after, after_tokens), + after_tokens, + ); + if !attached { return Err(COMMENT_ATTACHMENT_FAILURE.to_string()); } if !string_breaks(before, before_tokens).is_subset(&string_breaks(after, after_tokens)) { @@ -366,6 +371,26 @@ pub(crate) fn tokenize(sql: &str, dialect: &Dialect) -> Result, Strin dialect.tokenize(sql).map_err(|error| error.to_string()) } +/// Whether every comment keeps its place, allowing a line comment to move just past a comma. +fn comments_stay_attached( + before: &[(String, usize)], + after: &[(String, usize)], + after_tokens: &[Token], +) -> bool { + before.len() == after.len() + && before.iter().zip(after).all( + |((before_text, before_position), (after_text, after_position))| { + before_text == after_text + && (before_position == after_position + || (*after_position == before_position + 1 + && before_text.starts_with("--") + && after_tokens + .get(*before_position) + .is_some_and(|token| token.token_type == TokenType::Comma))) + }, + ) +} + fn comment_positions(sql: &str, tokens: &[Token]) -> Vec<(String, usize)> { let mut positions: Vec<(String, usize)> = Vec::new(); for comment in comments_in(sql, tokens) { diff --git a/crates/sqlclean/src/sql_lint/models.rs b/crates/sqlclean/src/sql_lint/models.rs index 1011345..d6053e4 100644 --- a/crates/sqlclean/src/sql_lint/models.rs +++ b/crates/sqlclean/src/sql_lint/models.rs @@ -161,6 +161,9 @@ pub struct FormatRequest { pub max_function_call_depth: Option, #[serde(default)] pub line_width: Option, + /// Spaces per indent step; two when absent. + #[serde(default)] + pub indent: Option, #[serde(default)] pub token_widths: HashMap, } diff --git a/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs b/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs index 55ff464..15dd495 100644 --- a/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs +++ b/crates/sqlclean/src/sql_lint/tests/test_layout_rules.rs @@ -8,6 +8,13 @@ use crate::sql_tokens::main::query_fingerprint::query_fingerprint; fn given_clause_layout_rules_when_formatting_then_each_rule_applies_idempotently() -> Result<(), String> { let test_cases = [ + test_types::LayoutRuleTestCase { + description: "a leading comma after a line comment moves to the end of that line", + dialect: "duckdb", + sql: "select order_id -- the key\n,amount from orders", + line_width: 100, + expected_sql: "SELECT\n order_id, -- the key\n amount\nFROM orders", + }, test_types::LayoutRuleTestCase { description: "a long Snowflake FROM VALUES puts each unsplit row on its own line", dialect: "snowflake", diff --git a/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs b/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs index 3ae97ec..3a3d365 100644 --- a/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs +++ b/crates/sqlclean/src/sql_lint/tests/test_line_wrap.rs @@ -72,6 +72,20 @@ fn given_lines_over_the_width_when_formatting_then_each_wrap_rule_applies_idempo token_widths: &[("__SQB_LINT_0__", 30)], expected_sql: "SELECT\n COALESCE(\n __sqb_lint_0__,\n b\n ) AS amount\nFROM orders", }, + test_types::LineWrapTestCase { + description: "a nested expression breaks only the levels that overflow", + sql: "select round(100 * ((sum(discount_amount) / nullif(sum(gross_amount), 0))::numeric(38, 6)), 2)::numeric(9, 4) as discount_share from orders", + line_width: 100, + token_widths: &[], + expected_sql: "SELECT\n ROUND(\n 100 * ((SUM(discount_amount) / NULLIF(SUM(gross_amount), 0))::numeric(38, 6)),\n 2\n )::numeric(9, 4) AS discount_share\nFROM orders", + }, + test_types::LineWrapTestCase { + description: "a call that fits after a leading operator stays on one line", + sql: "select (budget_amount_minor / nullif((power(10, minor_unit_exponent)::numeric(38, 0)), 0))::numeric(38, 6) as budget_amount from orders", + line_width: 80, + token_widths: &[], + expected_sql: "SELECT\n (\n budget_amount_minor\n / NULLIF((POWER(10, minor_unit_exponent)::numeric(38, 0)), 0)\n )::numeric(38, 6) AS budget_amount\nFROM orders", + }, test_types::LineWrapTestCase { description: "lines within the width are unchanged", sql: "select coalesce(first_amount, second_amount) as amount from orders", diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 9a02af0..2cda67d 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -165,6 +165,34 @@ def test_given_project_when_checking_then_reports_findings_and_exit_status( expected_exit=0, expected_files={"models/q.sql": "SELECT 1;\n\nSELECT 2\n"}, ), + RewriteTestCase( + description="format indents four spaces by default", + config=DUCKDB, + files={"models/q.sql": "with a as (select 1 as x) select x from a\n"}, + arguments=("format",), + expected_exit=0, + expected_files={ + "models/q.sql": "WITH a AS (\n SELECT 1 AS x\n)\n\nSELECT x\nFROM a\n" + }, + ), + RewriteTestCase( + description="format indents by the configured width", + config=DUCKDB + "[format]\nindent = 2\n", + files={"models/q.sql": "with a as (select 1 as x) select x from a\n"}, + arguments=("format",), + expected_exit=0, + expected_files={ + "models/q.sql": "WITH a AS (\n SELECT 1 AS x\n)\n\nSELECT x\nFROM a\n" + }, + ), + RewriteTestCase( + description="a file of only comments keeps its final newline", + config=DUCKDB, + files={"models/q.sql": "-- only a comment\n"}, + arguments=("format", "--check"), + expected_exit=0, + expected_files={"models/q.sql": "-- only a comment\n"}, + ), ], ids=lambda case: case.description, ) From a57ec8d2c278916b7ca672053f8bf770dd8691e8 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 18:57:25 +0000 Subject: [PATCH 14/29] docs: add the SQLClean website, docs and benchmark page - website/: an Astro Starlight site with a homepage and 18 docs pages covering every command, every Rule, configuration, custom Rules, integrations, embedding and benchmarks. Example output comes from the sqlclean command. A workflow builds the site and checks its links. - The homepage and README describe what SQLClean checks, without claiming it catches every query that returns wrong rows. - Examples and the README demo show the four-space indent, and the docs cover [format] indent. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/demos/check-fix.gif | Bin 213152 -> 212818 bytes .github/workflows/website.yml | 45 + AGENTS.md | 2 + README.md | 1 + website/.gitignore | 3 + website/README.md | 27 + website/STYLE.md | 54 + website/astro.config.mjs | 81 + website/package-lock.json | 6078 +++++++++++++++++ website/package.json | 20 + website/public/assets/icon.png | Bin 0 -> 12948 bytes website/public/assets/logo-dark.png | Bin 0 -> 19315 bytes website/public/assets/logo-light.png | Bin 0 -> 17032 bytes website/public/assets/og-icon.png | Bin 0 -> 101860 bytes website/public/assets/og-sqlclean-docs.png | Bin 0 -> 283773 bytes website/public/assets/og-sqlclean.png | Bin 0 -> 304419 bytes website/public/favicon.png | Bin 0 -> 12948 bytes website/public/fonts/LICENSE-DejaVu.txt | 78 + .../public/fonts/LICENSE-IBM-Plex-Mono.txt | 93 + website/public/fonts/LICENSE-Inter.txt | 93 + .../fonts/dejavu-sans-mono-diagram.woff2 | Bin 0 -> 12380 bytes .../ibm-plex-mono-latin-400-normal.woff2 | Bin 0 -> 14708 bytes .../ibm-plex-mono-latin-500-normal.woff2 | Bin 0 -> 14888 bytes .../fonts/inter-latin-wght-normal.woff2 | Bin 0 -> 48256 bytes website/public/index.html | 474 ++ website/public/styles.css | 930 +++ website/recordings/README.md | 2 +- website/scripts/check_links.py | 65 + website/src/assets/logo-dark.png | Bin 0 -> 19315 bytes website/src/assets/logo-light.png | Bin 0 -> 17032 bytes website/src/code-themes.mjs | 69 + website/src/components/Header.astro | 63 + website/src/components/PageFrame.astro | 135 + website/src/components/PageTitle.astro | 54 + website/src/components/Pagination.astro | 61 + website/src/components/SiteTitle.astro | 58 + website/src/components/ThemeSelect.astro | 53 + website/src/components/TwoColumnContent.astro | 43 + website/src/components/docs/Card.astro | 65 + website/src/components/docs/CardGroup.astro | 8 + website/src/components/docs/Frame.astro | 1 + website/src/components/docs/Note.astro | 8 + website/src/components/docs/Warning.astro | 8 + website/src/content.config.ts | 7 + website/src/content/docs/docs/benchmarks.mdx | 64 + website/src/content/docs/docs/cli/check.mdx | 158 + website/src/content/docs/docs/cli/facts.mdx | 77 + website/src/content/docs/docs/cli/fix.mdx | 98 + website/src/content/docs/docs/cli/format.mdx | 76 + website/src/content/docs/docs/cli/rules.mdx | 112 + .../src/content/docs/docs/configuration.mdx | 177 + .../docs/custom-rules/fixes-and-options.mdx | 154 + .../docs/docs/custom-rules/overview.mdx | 193 + .../docs/custom-rules/statement-facts.mdx | 169 + .../docs/docs/custom-rules/testing.mdx | 177 + website/src/content/docs/docs/embedding.mdx | 147 + website/src/content/docs/docs/formatting.mdx | 186 + website/src/content/docs/docs/index.mdx | 93 + .../src/content/docs/docs/integrations.mdx | 107 + website/src/content/docs/docs/quickstart.mdx | 211 + .../src/content/docs/docs/rules/overview.mdx | 271 + .../src/content/docs/docs/rules/reference.mdx | 262 + website/src/navigation.mjs | 5 + website/src/sidebar.json | 54 + website/src/styles/fonts.css | 42 + website/src/styles/sqlclean.css | 446 ++ 66 files changed, 11957 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/website.yml create mode 100644 website/.gitignore create mode 100644 website/README.md create mode 100644 website/STYLE.md create mode 100644 website/astro.config.mjs create mode 100644 website/package-lock.json create mode 100644 website/package.json create mode 100644 website/public/assets/icon.png create mode 100644 website/public/assets/logo-dark.png create mode 100644 website/public/assets/logo-light.png create mode 100644 website/public/assets/og-icon.png create mode 100644 website/public/assets/og-sqlclean-docs.png create mode 100644 website/public/assets/og-sqlclean.png create mode 100644 website/public/favicon.png create mode 100644 website/public/fonts/LICENSE-DejaVu.txt create mode 100644 website/public/fonts/LICENSE-IBM-Plex-Mono.txt create mode 100644 website/public/fonts/LICENSE-Inter.txt create mode 100644 website/public/fonts/dejavu-sans-mono-diagram.woff2 create mode 100644 website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 create mode 100644 website/public/fonts/ibm-plex-mono-latin-500-normal.woff2 create mode 100644 website/public/fonts/inter-latin-wght-normal.woff2 create mode 100644 website/public/index.html create mode 100644 website/public/styles.css create mode 100644 website/scripts/check_links.py create mode 100644 website/src/assets/logo-dark.png create mode 100644 website/src/assets/logo-light.png create mode 100644 website/src/code-themes.mjs create mode 100644 website/src/components/Header.astro create mode 100644 website/src/components/PageFrame.astro create mode 100644 website/src/components/PageTitle.astro create mode 100644 website/src/components/Pagination.astro create mode 100644 website/src/components/SiteTitle.astro create mode 100644 website/src/components/ThemeSelect.astro create mode 100644 website/src/components/TwoColumnContent.astro create mode 100644 website/src/components/docs/Card.astro create mode 100644 website/src/components/docs/CardGroup.astro create mode 100644 website/src/components/docs/Frame.astro create mode 100644 website/src/components/docs/Note.astro create mode 100644 website/src/components/docs/Warning.astro create mode 100644 website/src/content.config.ts create mode 100644 website/src/content/docs/docs/benchmarks.mdx create mode 100644 website/src/content/docs/docs/cli/check.mdx create mode 100644 website/src/content/docs/docs/cli/facts.mdx create mode 100644 website/src/content/docs/docs/cli/fix.mdx create mode 100644 website/src/content/docs/docs/cli/format.mdx create mode 100644 website/src/content/docs/docs/cli/rules.mdx create mode 100644 website/src/content/docs/docs/configuration.mdx create mode 100644 website/src/content/docs/docs/custom-rules/fixes-and-options.mdx create mode 100644 website/src/content/docs/docs/custom-rules/overview.mdx create mode 100644 website/src/content/docs/docs/custom-rules/statement-facts.mdx create mode 100644 website/src/content/docs/docs/custom-rules/testing.mdx create mode 100644 website/src/content/docs/docs/embedding.mdx create mode 100644 website/src/content/docs/docs/formatting.mdx create mode 100644 website/src/content/docs/docs/index.mdx create mode 100644 website/src/content/docs/docs/integrations.mdx create mode 100644 website/src/content/docs/docs/quickstart.mdx create mode 100644 website/src/content/docs/docs/rules/overview.mdx create mode 100644 website/src/content/docs/docs/rules/reference.mdx create mode 100644 website/src/navigation.mjs create mode 100644 website/src/sidebar.json create mode 100644 website/src/styles/fonts.css create mode 100644 website/src/styles/sqlclean.css diff --git a/.github/demos/check-fix.gif b/.github/demos/check-fix.gif index 7c22ab415822f37be264b422c9086239041cc7cf..7cdbd1ea2bdebe50d11b8d2e5b13ec12403ef1ef 100644 GIT binary patch literal 212818 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

    UjW46(TN`dV9mm)sX;4Y-CL!3{Ne~-tFHRLP)A% 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 z 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

    &v|vg48`_0^HJ5!8in`P2MC%G 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

    z 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≺KN$m@{?MWEF%)kLuuc{SG?cg2g};J~1H%?Z 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⁣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%0Ay39|D_ob(`EH=6J>b419 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 z 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

    %|{zi<}^ND`|%~XnDSV(UYz=uqe{W78vJez4w)5 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�@Q080n7hQeh~3-2#(vG)q=Mf?Xq1>;oGcVlF2J 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%0Dp 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≺KN$m@{?MWEF%)kLuuc{SG?cg2g};J~1H%?Z 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⁣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%0Ay39|D_ob(`EH=6J>b419 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 z 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

    %|{zi<}^ND`|%~XnDSV(UYz=uqe{W78vJez4w)5 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�@Q080n7hQeh~3-2#(vG)q=Mf?Xq1>;oGcVlF2J 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∨GW8tR;lloLHF4?a=1uXBy zvYRhe?fBSxmkJXdHvcX*n>yu|68T&1?Z?oDE>wwrg{(;M82Ik{5gp;N+kz2;omb($C7`_=xrvFm zSO@u}xb~zfzzrzO5GTF!uQ1YE>LF%hEbaTF`|-9<;96`SQ5IhL2A>FTEaHI(~~c zWqv2S$o|?NFOdLjxz<+$JQ&<*Qvg^cM#FoCv4vy{Kse7M-Y}~kUf94hxYc}PG)a|)145wX6m1s zoEAW@)c;tI2PmX!R?nxM?;4W_BSl;$O1I3owyq*hnSGIyI3&Y!(=N7#EN;(NE1ww> z^miCmk(QR}D3#Fy6}F9rHhE8AK#F>o%%;p@OA zi-@xCaU^9QfT%u%)f>GPai@og?K?^SHX)=r3`{^)-lBnf_*%bTNbzbv=yXC!kAI6xHn-SI#3Nuj^lF#h z)A8n>-Q|*YMUd&=;QG68oLjmGP}K-VkH_~6VW3$vS}Sh-7V9K<)MX}*5ms7fuQVC7 z>Ys1Eg&4oB^TzJuc*mDmWrAhV;mf%ZA_W{JHidvUMsz+KTyo@jN~C?Y?pY6kuWqmv zjLc;P`!10CQv6|aAZ_f^dZ0}SrRiTzxR~gox;PB>lFmM6#GbR0i(k}*EVu;< z*iEYDhqiscI#60%*YFvbV}T6l)U6>~jQu_tQ;Y}>q&hCSj5V#2PYiDRBhpyZOQ0NK z)}D;U2AqIjur()+Bus#~E5eCmq~pa&#)oVVsWhB8YctNo@oi00TfoEaTU4C+Iy=A# zpp5}0@Q$j+>vFI+|3YQWOInQh$R^;#K5z-$NR_~lP@f`;yx*PM*=jJB2!vJ0tiUj+XoFNT+xq>{-Ru2I zNkvJ8(Ei&+Jv}s|Ad`#qQ`x6%=9l(gs1)BmQx1(_A}v42#?C1ZTG&~6%Hr=cQ9OuW z(F(+Y6B?v`2>NP6wUm$KwHRHUSP4gWrOO98%tE3;4Dy@PzeLrXjDx|i)*i(LUw4~Z zg=yuJ9{Lq>dMTY6)A>VB=R`EOQ04LQsWMB~okQ7hf-Czk&e{I52LYslNthm1PJbGVKl`!A%NVq{l4EFFXhsk%iuVvLG4k$qRq0o)OH_Ey@J!eabaV`5Ny za|Z?*QNutZSn)*4M7+;z*h5|6Rwb_s-YgnrA$+&jLQUI-LL*3-pDoU(vym53irUDw zVt!FRPd`XMzL_196s8Hib8|A1kDi>mEMv4S(^UfZd6ck- zPLO-H_%G&_c~r>PNr&m^g7kxBkR;Y7II4T9ai3)&l2<;nzw4rXJMaJE@#o*Gc+3L% z>|rsL&b9Ufe{2>XluGK#>{H7-vfGJl+aKs3A&2Ezhl<{z^Y3&Xd|CaXbD>6dFNm3; zP5jjTlJ=tD^4s(Aassse3$+~6u=Z}3h8?+0m7PejcCL!`Q2onCgA5Xz0qe`5!*A5w zLPuE*n#^UA!r>bsZCy|$?;f<|+41)==x3kLLEC?BT-3>EX=X*gw2K2go_M$0VI;%R zbXxb7*W2-EcDZ`Tb9hj!%X=WVBEiT{(G5PZ`j7#a)#FFM(SSW~Ik;VhKI6;`*cep$6sn02CYN672I3J_=y)sd1%Eb(_K-eO+J(VEfqP|UI>T#0W zqv8dNEUQ(OoV8U%Z!?^UJ9`XowE7#M5#1e5wN_TVEUd8WZuVF3Dl*I)-on6+{`Wv$uV@VqEf{iRzzQIA$ zw+$1nPbFQ=|12S!#5Uy~_E}aa6*1F6b}grpO&&|w;GP$YQL>KUIhJ@|*o{<>y zI~t@OH|rtwzhZc2s5NW%E75;<*W3Mcd`u``mYWHm-XfU81t3;241Gn?Fd_GBFG=jQybNfpbR)$(V&?){)z6+mdFH;h3D=))mdg znG?tY+(d(TY=URr$Wv#B$2eyAT}JJu4EDt3GOatmwM-w6e#t|r+aVcp+^UCjz9LdW z)5LN|68)gPj#-9u?U8uB*WfPcCW^p~qXFbIcETR2Q@YA2s9!T5y=V)@e+T+8wzIkss3~|VB9XOw?1TAcGtB9 zrb|>j8IG%fy%}eS4cOHHwTxK??y{9XLsK!i{O;(ivyp^SQXS>|Fqj|An&1&NFTdZC zMvxO~+}ydq93u87^?vo&+o@xE0>PGj4K$>%Ed=+KkM!)G-E)?}7paIcFxfD!{PoM` zB|Pq-lAF>U^XKQcRKq=Eabq^Ll(3KkUU=JKJ`IA~1bO@*aO6yCzoSDzZtIF*h=;$@ zkt3tqf=Hq`P1=7_mKf~F|ls~4QVrtWV_uYQEs)^deYeNlrIn%B6 z${_!g;mb>`;)~47NlL{u1TPtxr*LwKlrbVtJPZmT>zm5o%_HD zy?W&zPMaBCyS`WXrZ@G2mLXMCN4xt2S?2;?ag+PMEk@1*Zg0)-LsR6#fA5_w%uibH z;w>xwbQl+y50ZeixGtGj)Q$z#w241fVTqxymsmP*Dk{W$OKEl!F?}@?iGbg!yyq?E zoT0o{7DMN|eUKuO-jOQ#>xW9qx}pM2ou5wzLE@Q8KNEgQW$-%wR>}J8zJ8MrKbws_ zH#WCNUc{aJxvFJwmAqIld)N{v+x6q*U2EO7choKyhFWZsbYTQt_yjFpPmZ=jz8yV? z87bdw^1nO6$YofU>y8r={1&`SFR6dJF|j>(K>Ht!zrqWAp1~QeJ@XEV4p|)j56G&U z*7~ci4dHMJ7erV(5w-+LMoaRPvhRI#6M&_#S?u8*1@28)`Z{(&!#~mRtEJr!V+ly3 zH>#@#SjQo=E`9_~S1!??e28*s1MDY$f5#5NXrFga4@1izN@r@NIM(m+Q=PZ*24Bac z_FFuv#G(2c|Agf2VNSPkvq)bkkBHUR3(vW}nw9;B(Oah7nV1a#AP;7{c-ed2ykB@mBsQa*sMExK&PT-K7syLU@}pVF&zie6x;2v11GlH0eF0xxuM3zbVO zS@OxFBjJ)?VD?<)@U4OtTz$oy$&NKW8lG+JA(?hxp8t&Bi_o3PfQCO-+;lNa3(X4r+LTW-o5^2)S^3450keJt8EbHgh--=Zi; zIX0WB#_{~IeWp5p%aAhWIkiJQ=gN4V#UaC%&PgUhOH6j{0yeg??7o40u^*=k&1h&Ox3A0@=$WwbPYQDWM}I)NpoL~1^#@F$IwCV?lU@+`*TAch_R$D zGUHdELo5*^F4u=6>EUw-zP>T|~z_5~NyQaZ(&GH;BHyb&$K6W?v{W zm38~OKWif~o!02%Ch(&0pHI%{AB=VN3XhS@EzYg!&FPN~`6#B-Upq;Y@8Rv)pXVOy zyJxcMixu8ghTD9^y|_Up`D76a+iXsuyKPGw#f2q0F^SbgR)`fZEOfF~+muvS5afTp zlO|gX66~|;FE%8>&P^}={Y+F_P=M=*?i^|4US>70F6>uDt<3*BBom`$>qhPOhHR#_ zW1SP{FygK%!1n&#ERk8LSNkb;ruWYd zpH)K4&8&YRsv698f8dpujLvuQt)qZ^pXiJ0{Fu-ut4U`P!gy3V7>;0J z-k9~*`e^7t_vLIc zKwyIH1ewqdWvbxH(DhL?u_1&6%UseWn9uquIXkT(td>QGiRBYg)g3K@QaiRMO>NgG zHT~T&{J!ABbq*n|vix*MpGY+qd`o=`zrc3Gd&zfxb$^x2;r~>sORsX)Z}pl~7P34# zT!trKXGff0X=aXrOQxa<51R$zQ+W3hqSGI+bo~b3}ObD_mqmVcE>jhiC>Jku(RQiG^=(LCdJs;G&;v|HPhRUBCtJ&!&e&>jflg>7RtmRWJER z8H#IO)?tywU*x#8I6-s%_Lr7SBH02!+xRRhFW*_jBSJxKz_tYk!Bf2T0AaM+&%9um zuyQ7sAnTEw5}uVOpRsI*7R1eOGR7W0!2lS*>tzjVAG$UjXJ2HVTSFp7+8`?)UAIfa z@4FbIF~QNU*h`x%E3#_Nx!S3XQO@jp=` z2|7Rt4`0cboM9Ztlme5y=}uwDPjyCr8m-$wG4f>2ly|3t4+qtaX+VMsLY0vN#Lje6 zVx>fRcsM<1X8f%rr?w`8a(b}p#s2j3ag2BfCYt7mVId(J_|Q}$KKa6Yr~XXcX(ucp zMQUF1%E|90u>xj+RHs1$E*_)*3 zS>Jbiq!m>$=VEaaRZ?tEJvEWb`=j39T?iV%k>R`E{#Ks6e~SY{njxK*$_SY)v=$~d zKtBKV#erPkYIx`E^5=y`Z#a}~!j@bYudfIRU72wgdap=XUG?UnWo>6(e!tS>_griO zRLG$qWmD@kminezVrK`Egj;_1%1e_dj7h+V2`f2(TTDlmlb>2h?w`)}>K7T}RKDF1 zc)jGZub>XnlyR`yQwLbqVS=H8gurEbXj9ysF zEvMvD8Rnl|V!sb#FQl}&PG_s|B52CRk6lNWIU1fn`QUv+JySRb_i4wsAcyg?c_g05 z%BY@Tm8w?K=}>H`guZfEWNc*z8lQ6Qc0{bGb^9x(tfMc&&x;!Gyr&t$u5ZIvkMs!x z3pR%3PKU5e@e5&SQv3XgCg3BN!hp)Z_e)-NbNS=nP>U$f3 zv;k_R%H_$2>0dxSo<$9OxO>?_A)_CZCXiM=q_XMvHQ|0(R7@T4%t|IFBU5;U{`GUW zXGcB1kO!^ZZ-9tVaR>OXGI&rJFq(vPcIh0@RKDWN) z{*o{+Y%fDAKXsnP~Q~<`D*SN8O&z2cm5&KExsczpa z+x%>AuoA%sH-d9HR7Mz@iX8;R=vYZzQchvwazf$<DjCG>f9RzJ1St>OZg(n26qXI^I7%@dgV#&1&+?0+uZu(AVTguN zFIL}i0=(yuWVn;G01K%-xonTSzUhFDG}aSU`x}g=qUNLk+ER4nyWs?_;s~<6qrkg) zEoXVOZt?ik(>tmwCYK)w9&RxkfEsh2>PKEa$1bwYTU5Ji&2~3Nzg)^E0Sx}~oyU9} zGYeoX)G4@A9499Zjl?&R|?fu?B$XBut8Fy{6f7l4%NgqGpBkq42&`v4q z4{sLYI@h4BswGA|2J9trIXnfqRIIm(_TC%>ZZpU*Kpz4Tgz#MXUywb8#}pd3XYEX~ z48blO8gok|eC7BS_ zUV5Ey;I4F@08Z9b(=*Czy@Qg*HENcA{v*2|b?@LgQ#Si}*%GjRO^ngd1dKGUvJWWv z+IhqL{I5*yP*4ro=Ys#5@oFXPyKY} zK!~SQ-ZULzt${j}EYL{|@gFBHjGm3=C}T3D?9`c=UZv7#P$SmQ>v-DZyr>76%dTg3 z7{Gxke!mBO1jqrxZV)FB#rL2rML}2@rw%a15#$%ZgZ+U3p2VYnIC{K#{?7iv8)n5g)+X;2LJe=AU%N6OfQgM;1e(5O#25ZU%(2Vd&l7O zS)M;R@vL86LhjRA2aJ3Ra}tpL6LRA5pJVO2wfrMDIWBkSTMp(EkH;iPm#HvOxykbE zxqf6Rjv;IxbC#~{Nf+l(FVXs(7#1)`ZFPNb{s9#;B`0O3-%=!&25fL&BHp&u>0pa^ z!>xZFw4f+Pk$2+p`cLOo`?qoyA;H@H0R2ljXDYj-=lvQv`Sn|gOKi9ky@@pfiotAj zQT&4M%ZbSep^y*u0(N+Cg)4o@E|d{H;(OMvrM;4Qls}dQYeqg9?HX44{yRsw4C5t= zt`yjz2yCn2v9u=V*%Q1!LU?!c?y8)*#&zpn{_RS3L(g?m@M^buEmix|*Jr{CGk1F< zX*@`65)FRni)=FJ&%$p9TG!Xt&Z3O5qOBUcFg^&?DsN#_EUYdV(1T=F|^ zM<%j!b;pMIwa^Lmk`XOx;LoHx%Fb@Qd9f<{J%F(i=1$&EOHLHWV0nez_io>lp8JD? zcLSR)>#^u=XRTg%*w%;!ddv4w7U2)qnF8XBuCi-KG+pCl?*OVo$gN_92oKZV=bkAniu69Hr9wY?!&xJ==5zSZIWVdFg9Ls(#98khW+y9m;p7h~Vz z4=wY_DVXN~s=TkAPw#NWrPpBBIbSbHi}ljWYZZS|(@fb;n8O!vdDRW$8c2pNn=#>D z;GHojR0+b2y$vw${4l|+m%zEh5_FL(i<9$Cf{raTgHIvtpC{>6nLAF#OUFmUiF80a zh{AHY$)+4*n{xvYyL<1KkXkmsvd3v6%%ff8f+^C|sT|%sT1fcHn9D1Nx`;bExRMuL z_MsqnlJ!K+Uhg-BGA{>atOic<4tLY=;6n0%MPovr{ihL(&;LCrK0j7jEE;65R$ie$ zV#=$VnXOSeil-=uqk*lf*-iPvW80r+pW9Uht=Pnaf{{zb^GPm9p@3?af}O4%x&|Q_ zQ)5TmNeF?(V>;X0n-BNj1vlK3Uh8~HQ}zO{X01B+>;zrPR(1qqqTJF3uQgYV=6#!T zo{hPLk)jwDKj3N9s#G{EkUdx%aMIw~rIh>c%$Qyrmgoln&#t$%w!|E3tuHb_k1lot zG7hQXay^$)&CH*aC;8>f9<_xR8-&l#(jpC?k+_HY3Yy!G2c^!5sLBb7+g>$(;>n#ncMEvwPxe}7GU#jnK)V zTjShT17$(&x0Col;p;tq6B_0mTom(RA=AiDNZiU1yQ?o}CZ6F+&U%}}Blt4>3G3g- zCbOm`rtvsvgIPh0KomU|g>#_B_;%Yr>(E1964&^{hKZa=nNhPODwgfJ>#v1N!duUh z!uj%KU`c`rZC_?A^N$B5JLEc_b&snE5byl60^_AGF|lXh9j$HEiLCSIeXq5$RvfKV zw623M_;rLH480SK`S$7|(G0V_)R=Fqd_~reDG2}L_77$`j9KD+=-)vWevbk3exnKV z!YA=b2VPnhX@L!-`0n#lM?DR$JkP8ix34?H30D#;7!BZ=jafQg1c0^F&$8nCCP1U{ zrJ$TPsQ9p`wj#~sbKM6;1p`&r?<1P=pEEGPW-X~c--tEqB7}}{jq6bsF&C9N(?Dr+ zSRdV9|Ly%b*G{c0JXf(%wT0u^%qhUWVDsh@AeCcLkSrpV-Ub)5&BPTcNRiP>*tYEM z2nDE;UNE-R>)VlT@j=gL<9ygS?6HEi5EvGhr8d@QG^-4)He12a1ks@%+W-yy9*Hp% zy`3;pjPyzyz4h?q@doA(&Pyu-ZUR%ta~#gCJ$=;>AU!FU&n~RGm);1OKlcblsl%3N z*zvtS+E=TqRIenq4|ofwZl+FvB{7^fAKT2Pi2gx1p$!blfA^k@4Mp-Oj;2 zIwJ1A#^|lwQOu2svW3l+p0O-Aw}Ke%Bc2=MOzZkm+Mk$Dd1Cf%+B^%Mmy!mS4o*dE zvf&$tJZtRu&+|rZuRR@wY$=uq$4B983$h)j=|SYqQLj+Jljo`(`svqIQ{mF&BcNn0W2mtJfhFVgT$l=>(Hyg6lAKw&fa<|Pn8?{-#_Ty08t zi~^vf;=H?l!s@aa;!=gQnUxy2$tKJSpiRU48DY>s<-;lweKJF&lq!2PH!0Bk7Sp*Xh$z=xSGXz*B$vLA>GK;=N)fNfs;MV?} z!BBMf@^squAv&eb7#n zF&foFdY2%I8`n_d)Ub5=BOa+CvoYf{r{PGR*R}pEWUhhrts6yYN-9bbB0s2%ajYdG z=G#o)MhYlmP^r_xgY_yJJu1P5=&uLD=gn8AIUiq}h)owhHZ=LRN%F|`W-hlj;H2tQ zolW#h&(AQSwvPU(OjX=7Ed?G$Sb^(?@xFmF=4n7y!GL`_tp)+jY#V zYIUdTs@nDsDmV+TPjxV;yrz8K^b4y*lLA3ulY?3!+SRJLfPZ46C?rJ}5w7l!ke6P)0(l-b zA|09i33Z3v2VcYeO#4P-6oWf7>#V028 z@#&kl#KLOngS_Q>R0I+V6juTQpf8pF^#iJ|<5!e8Zb<&w%gd98)`;?^mg!=ReSwntmwq zzr+*QoU_Y8jlr#NR(bW&&U1^bPd(aKW5e6lKME{GSKWn2pjjGc*GaEbUAx$znd?{uLIn0+DgxCHWJW;$RGpg>MJ;Khdyv zp-)ne3hOhzC`HSV;3>|wKI44q9Xl^L>l~=LeC(~$1pGVj)OP>uA(;{ z-*9_n>4FuBdsZBo%DtwbtB*lDmdi{h;HyvFqPx86(jHf^M~+}r5h134jOW-=A?UTXhb&}h=qN`O&?wPn*I_7Gv(ucbb2 zvKa9-j`tK>Q1&B^{R*p6%*w_GS<=USEqD1&khCv;MuQ%ABxX}qUFFE?2MDan4i@eC zwxrFg`*|GD2>m*x=NwxaLgj1B&ucliAKeTkI$a7LfvL zw%9P8jc-CKj5njLj3-C5z4LbZ#$Qy+3qK0FgSq(LY+OQbE}G5dl}<1yaKVnO7`*AVl^`# zXLx}^Xe#x9o^ldp=vca}T)BI|&8F!30m-$to>G2Hv+W%_Batopblsm^YhGAQSwQ*A zpSbKeW?@ZRC+|NyJgeouwR(SZ^tyky`g_{`T^# z=4EHgB)1)g35Fe{T(s|RmU9$;h}A)|KXy*b@(Wz3^N<3Wmrr#qRXd(vy?$H3i`_o* zW<>mrz_x^rqxZ_rV&u15}vT-a*qP!=ej%$RkUn`R7Ch!)b zH@44>)d_i{-pJlD`o7hxNNVv$bDR$McX>`_-s35}P-irIYWaCLo zjhQD$Zu7OI+g6B^rmahnwLozH!z_NOGZUm1@E_T7U{$4QK&t`IofWgS-cP9U9@l7` ztlSrN#_rFK*+^)*e?V?O+0F%eJ0yv-dy4U`kHSjJAJ+AAoNl+XJ_*U>Y zktd$;Sij7HFUK(X8sqCu(S=a{B4+26(N^2b|8$Q54o|XQcJYhUKCHviXd)4R3d*zIbO1HJr>48d| zHb+w!QFPbJYc*a?-WR0waqpXdsf`YfzVzZBHPrbVDU5m!H?^Rl{@~(dS3u^r#&7=X)qi5f+sZy!KEY!P_EJ{Mu&H}E zi(a|cr&!owJaTo$v{c*&T~Ib@TpQ+8B}HMqAt5Rd-!{M48CI zglWr1mkz6_P?*F&$_`<06~^F5*78Zz#YHJq@kU<2<9HdBZzg@m`yER`zjwMC%sYQE z7N2WR?z4#4kXiSmg6TSHv{;gD->K|nPvMSCg4~=}qy7bCpx#-3 zIzA%`?E|LNA2|0@P20pN{ycBdR>6RtiFZr^cz$l#g&*t(Bq&lzBwnXnF*RmF5C?I5Q3RML#qKb z9eA4`M|p|~8Bkj+#*9~=%{I2{@Ak=AO~1LjyQDtdRG+{?64|ec?K1|+w}r{Xk<+m9 zMc<&g-U*j+Z=rI*3}d^>JaB=D3pP?r;ULO$Hi}+dQvF$lu}Wt2?-JwG>BT<}D-~lJg-! zdFS5wapBfS_Tvi!KJ>|H-ASIu9u|+I`J%#SU^#V5?)z?4iz-9AhDFn9vBS`FVr%&( zJGqq3)U9%(dP!83g(pWOa(qdm>M#(DM}F)i^=vo@Sy8el%N+lq@R8sXHPQ?wnRT=A zc_=N>X>G31q-({N;oc7M={q(vzqvs!4jSj%VeaP5&p=sr6dt(*Td`ad#DKwYyx^-Y z_;08IHc8(oP@ma%4B8)4}^(LEZP1EWkO1qm<*`y_#iQcbdNJgyt`P$a_g%Lc|s%?}oKy(GGN;v_^y% zVuM|C%irQ2ttWx$*lI7K@@>Nhh9oFCwzs1!d45)5S&8>-UT<=wFf`*ewrrg1!+zKj zRt!L5t$va{2s>ZP82>Q$wiszK$Bh#`4x-{J09?k;;h){Ox_bzyrHp#cTj+3fAF{EZ z?isnk4^^fIv@{PDziBfL6eBmO#{KCG|46QG zY%M~38mkHmS7u!~2kS+jK(Z*tf(k@^1&`CzGk}tuQu96rtbbpk;ueTr()10*;Isr> zR9=ij@0o?Biw|kB`U`F!i!^Q4x>y$O;9p5Pn+~L!$zbUHZY6i^yv^7I=dNo;MH~mR6%mIKaK~3e}6Z7B>IoOqe6_bDd3M#~7^4oE} z?=+M>*7ESO9OPYr0R=uK!h+|PRc6@GNQN|}=uThBr^DBfDY_SVTT}F>x@Sm756;U_ z;x(#!Gu;!;l}q7iLvA#Z=!m--ebC?~ zhoaF0Ihb#xSXnDhE&7g?)mo{tzCFFg%dZe~EAG4Fe^d|s(>`9sx+2%?M@drwiv2>p z=(HuWdB3dOp+a1Akv*D{RVg(=o;y^?tH{O7We*-ZPTk z4$$iRzFkJKpIRs$ZP*+BskCW(eR8FTMlb^5vIDwCHj$7E^ccdXQ^m7sQX?L{&i;=- z8?oMeUV<0^y2=H_A%$`gvvC zomDI|Zp3K55n)D{nP0jcYWfe^6yTvv#OWrNiOLhASDM{8c9H7NjoZr|VvtLp6^>^| z51OZO^V=lLD?Rv&+vb65R}2Ta5LDtfn3vqVo z&7S_l)+4XBYH%O3V=GthBXjG!5lm&>fpJ@=W^r8_ZWFyEnQwqeJF(W4!!DUyt7XbH zl+{ju(1ne$`anbq+$;K@+s{o??b7^RKP;0Dgg)zFv6x;Co>{tWsYP*X#bWb|T@G7r zZw?(4r4{6X9OyqkCpx{u^|d2WvxIEo@2)-rv*YqNSjiLL>3&$#;=6`83=RHAoq}%( zVfmb^=7{@+r&kb`Uw_K2Y$r$5+XEXXcEu|teoXrG6$5;p-9wMeg^v!|gM6?8_;%FpZQ;2DZ`h}? z)F)=;WpX+$_Gv}OzeCP1In6udo~(>Ft;9DSI2Zd=grky$l-x+<#mCjU@T}*8^#eS zaj)-pH^_5>U6eUZj~yiVMhJA+ zB*s8~v$gz%4JGAQDw*rHoRofd%N%CdIoo+wl)N02THgJ(`)CaZ{jyK1bwM=3*u8+f zVspd;WiVkMaT`9CkkU~yD-eIdTL7ZDv=qe&^j?fhn94A%Z}GG6Qmd~MiJjja;H_}t zfw$%Bp63BKaBfUB3-g2`Bf}}My4wqT;P5uR7WXY2b;7t0U(wtm^B1edpA*n{F8lrs%&}UTIo-MYz-~@W<>jY+>u~B9&dh8kzK4MEA;#_$p9kWckdG!N) zv=2RaplQ!bsc>`K&QTTH++W7+p{4~N1BKhizY?g0{+IXZEDmwAq~~N&4ZHXyaajtansV35?Wf^jv z9g4zvgGjr8sMp2q^2$?+4bGJKqPD;0EA<4G;!YJZoDO2DT41|l2t$T}fUf3;|aciMd@NmK4i0dGmrTP zz^v@(;}cPmcFuD_ciMzO8!(#hW9QL4jh%6y+3J#*Ix|06*YYM$R?08|bfCGyRJ5{F z`FmKpmG=~(W5lg4!Lqkoay))J!#(&!%pB5visBA=5!<_QAlEj~3SJSHRjuoDM@QZI zU&RJ%kcAb|WgqTXTu}R&3>tI%H_P^qe~eS`*wEtJCV%_^zh}2|$#F_5la~lZWj%ql zT->4e6=@O6u9*3?n&xrl9iT>KOLC_ha>vh>H2V$2qvthg$RRxlw=_Cl^5^WmE5W@( z3=%c4LJ@dT;aByp4X8;$z=z0fy33yrW<>6MUQ|B4yp zb!n!dOZTJwSRLshO-oS@xAjwxEq4T460V#jHtTQ*d{F+tl0*t| z%`}ECj{DaA-a$#BA=^dMK0e0RPe_O&sC((2@Vk4;;WxW48Ffi%{Sm(%3}fw{f8vXAFR^QezreNj zG?avvifTO2zCJK&;G8f*+76Hu=NZarB;#~bubg*JL-*Ef z1m_;&0;y=XFd?+B`;LW zTgmv=a93#}H12-pu%=R%MPaiZou@LQw%5qWJQ=qpaP8d*Mm6`N<@iDIf*HQFlkeU0 z0lLi5@^lwjyW7X>;y(hu_uQI4-4Yw?vmiec4hdNs#r1so`610Qh?0b+6}3I-V1T+m zowX7xmoq8z_@{Kk4Vv3YukzssY^jZ%z+Y<{#yL7%ns4hu2^^rrOrflK5m?oMq^vtdDh)2`oZbTqF9XfaUSlG3Pjzc%UNQBZ%FAy^Ce4JaseenP*OrZ@8i@Zk9?r8Vb$5LH}ZTvMI z1%!s?mQ)568J;QL)qZ1iiPh%tcIx!L=3xNlAkSZc`^H6B8#Zc-BU=)-x4^EgTMZTv9U9-(u4}L1gmEOViMkLB1Xei| zlM{M}hT-QO{2|PVJ)Zk9&BBqs>69C!mE5&Oyl}FItGP+P}k!n68&OQWFhI16A;5c6~3)TEiDx};| z!Su(PoKr0E7sv6N}`pjHAdDJ-5fZrgM0$zw$FlT zpM_4(fN3*DQYe=l{#Or(KLBS-XT0sZ=W#rB)nUp5cW*^K=|BU!KUllkJ%5orB#N-K za_+ra84Z7kwN2>~I*QkI&97=z!TCN~SB6-dbQ6z_LVDZ`D_Ze@#Fv7=cM$Ss4as|k zrhn<(YFW{&qFLo!WgolUOxp*kP9=3`91|Rb$LUR&IC9*!Of0r`Xt5KSs+Jn z7W~hi{m9hXgF|p#@PLff*;Y&e5yAveqj+?1NcW4Qz@sySB1$3&D;7s5Hj)c~Q zS4fF{Z-vo)l|wGN@b}3K-5HbQBdTren!XH(OMW4cY=M176fYW?8|M%onMvy`tr z2~bWEO7gpLmP5IlETyxl7a=vDXSJ^-nVyu)HH7w|fuL(OU^^38 zZRiUXJ3=p-kZ_w~N&>%gVjsDd z#1Sd7Z?pdnKPyZUf2z>8WacD4m zkm%8RLO{!Jk0j)e#nAEWe0%oGpjyE2yUBMH(m!(=8{(SlPFm3QNEhI&0Wm}c;xtuT z_y9<*q+Sl&fgDAYkljdV5qGzY0H!oo#!^uFEbC2yN$`NigJ;q|tUW zzvJ;A9Wrq1gz!k4s55CB+s~faV9ts6?T(~1Eh{7yhY|;n=E9K7 zSHH`5!?-Ep+&b$&nI_#foNBP{d6@Y=piVLHl&V}<`BL*NUAjod?KT-e1F641#&NAP zFU%MBKe^n5f^CoDmQOgAm$^p-JSW5N4afJ+iky&m0?Bgzo-M_tKMk`HXMJ;8`ETqm89M)JMmgNTtriX+MB;+ zsE8l&N>nhhFq9F@H@05&(@AxT8Z}Tb5I(DsbW(Nfr;_`w@H#B zj@u~9SEz_u;>{;Gm9*-D0jec0S0A7of-hX)?&T#pZ(}?)CWMDCp3SYAf*Svppg^z6KrnS@BtqyIr)wVV|udDSjB8IzdrE@x{VOv>ajPdTVm>J)4*8!DA`s;t?Sbb^@l` zwDlPPthw&#F|70QSXSwjbw%Wz}d zAF;U>Y#bJT`F>zMgsbtMBQ1avq?7IPx`HB>AD6l+?JH(X}(~gYkx8 z^>+}zpHOd-Q^QTSEnJ>54mn?5j&Q1<@QcGsZDEB$fX}dSrWwT0=G{wztoU|K?=Y~|~%{@%f#ONxjl_r`m9_ayE6xh#k()Z*||p<~KTi5j|$tRZ{K0bMSQwO8nQBOOGq7?p~Q>=R)BK zriX_^g-OmdlgWZXO&IqxlKCaf!Nq%S_-uh(k9!T?+%YK!hh%v@V)T?tg(pXM&&GNV z8D^R6$yCrX)Z*OYsRX*vuZsF`|F9=gh#{`eh5g2^V9uzClIVk_J8A2u&%u4}X*feh zdYwNtE}Q{MDLtvPFni&f$sXYE`<#<5*bm*o%_l|*?_1D@x!nO>czA@%hhnp?VpA| z^X8pQ(=@f=Mc<0p${UnPrA0z`NzNEVqpr~iMZi(mEVZu7uTb)ZtJA99p4R>zoc*xh z=k1$dOrlq+3fD6EBv&02BLH+j$m*l~AiyVo)X2jhj~7f#E1$nU z2#qm?9>|-#PLSp?X?jwxB_DNU8i=6qKkGo9_%Q=c-b1f*dU4fvstc_*DqBPOHJ?d~4ko{1O$~{@4NUlgU$y00Ge;RXz_I!C2i?<}LL%IhnTr36Ec~=ozj*xHepCM0X z6@tRt5tlQk-ltd1bNflUqaL$fce9=KZ5EvqjyX~Wj&YI5>SKqxv~!zZ`-L6lA#3r< zI@K&#KH!u=bs zn?*sCe&OmR%B4SU*2-`+80PQwrz?tPIJrDS;+0>VyBSsG3DRZLDD>uH3lmb_YcsoS z#$0XAJ=pqOm1GSaa1#4=GbvJ#h%lWsxMSeyN16lKhvtXoDX-kzJ;={L9ZNT!t!KY= zllv|-Ga_$0J6LOGhudv=v=z#8UfU_yf6^j?Y_mt{t)_k{}`RYstlvD$IN zTlN)sOkmr6qdANRP$pg~`+}FBU)aOZaU&7{j9T;Hu0vOZ36E#l2ea-hyXe(nIxT8( zZ>(x3`yppixU|FxAV7k%b3Rh*BD9<&<0#|wy(&yT-)Mni;?kq(Y^hfu{di<1d6h>q zR1y;)zL_Pk@kKd)+LWaZixta?GUyh?& zro7#1_lWQ=ofYjZSh*{KyHi*FNW_tWzeb@ z%03P$&96b*Ym_seX*ZU89UTyUe|UtHvYvszNiJkb3$aWB%k`n2xiPf=ji z`ElM>cMr_Ub3JkzjM+%d4m-R+?Dtxgo3;2Z7yY2>V3#-5^J~q81^xX-aLh|XHrcqZ zUlXLnYmSrw09xFCw3$?Xx8_dv((Fo#9Ev-;H4OFc!i0)Yvc=OG;YQ_i89ie#t+@E7 zOiV)?!KPbPniDd;K{amRGd+#^^p;)wG}ScVsmkC`wj#Y-!`@jxYJRd@c#CCUZ|A*R zI>+RY1Igb@Kfey42Y{H91z^i74ZXPGo^SQ`Z1!v=!|FooR}me*T+3B{yn@ERh6~ez z;sZcV$P0rGDsZ%2%o$5GBbMK`k`%=sO|uW$-%q$^5yqx_@$N)y%IIO7f{Ce-tX#GY zI4BueJpT=$8|3}y>D@Z^PZP)JzNzF!R2X)WDwf-vPaey~TfrQtbcdtZLZ1 z`FF(J1iQq4$D{w99&GaeH4Zoo|JQTCG4a1)1vbL}CIzq&{x>Orjqv|#Qb6Hqn7&FI UnPe{p&jhGF&{8Q=wto5l013Z#p8x;= literal 0 HcmV?d00001 diff --git a/website/public/favicon.png b/website/public/favicon.png new file mode 100644 index 0000000000000000000000000000000000000000..48c19ba72b07d2a8e7b54223f13e6e91869577dc GIT binary patch literal 12948 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 ZjGyMV)hcwyf1VA1jD(_ittc??{{Ri?`+xud literal 0 HcmV?d00001 diff --git a/website/public/fonts/LICENSE-DejaVu.txt b/website/public/fonts/LICENSE-DejaVu.txt new file mode 100644 index 0000000..b3d93a1 --- /dev/null +++ b/website/public/fonts/LICENSE-DejaVu.txt @@ -0,0 +1,78 @@ +Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/ +Upstream-Name: DejaVu fonts +Upstream-Author: Stepan Roh (original author), + see /usr/share/doc/fonts-dejavu-core/AUTHORS for full list +Source: https://dejavu-fonts.github.io/ + +Files: * +Copyright: Copyright (c) 2003 by Bitstream, Inc. All Rights Reserved. + Bitstream Vera is a trademark of Bitstream, Inc. + DejaVu changes are in public domain. +License: bitstream-vera + Permission is hereby granted, free of charge, to any person obtaining a copy + of the fonts accompanying this license ("Fonts") and associated + documentation files (the "Font Software"), to reproduce and distribute the + Font Software, including without limitation the rights to use, copy, merge, + publish, distribute, and/or sell copies of the Font Software, and to permit + persons to whom the Font Software is furnished to do so, subject to the + following conditions: + . + The above copyright and trademark notices and this permission notice shall + be included in all copies of one or more of the Font Software typefaces. + . + The Font Software may be modified, altered, or added to, and in particular + the designs of glyphs or characters in the Fonts may be modified and + additional glyphs or characters may be added to the Fonts, only if the fonts + are renamed to names not containing either the words "Bitstream" or the word + "Vera". + . + This License becomes null and void to the extent applicable to Fonts or Font + Software that has been modified and is distributed under the "Bitstream + Vera" names. + . + The Font Software may be sold as part of a larger software package but no + copy of one or more of the Font Software typefaces may be sold by itself. + . + THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS + OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT OF COPYRIGHT, PATENT, + TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL BITSTREAM OR THE GNOME + FOUNDATION BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, INCLUDING + ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, + WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF + THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM OTHER DEALINGS IN THE + FONT SOFTWARE. + . + Except as contained in this notice, the names of Gnome, the Gnome + Foundation, and Bitstream Inc., shall not be used in advertising or + otherwise to promote the sale, use or other dealings in this Font Software + without prior written authorization from the Gnome Foundation or Bitstream + Inc., respectively. For further information, contact: fonts at gnome dot + org. + +Files: debian/* +Copyright: (C) 2005-2006 Peter Cernak + (C) 2006-2011 Davide Viti + (C) 2011-2013 Christian Perrier + (C) 2013 Fabian Greffrath +License: GPL-2+ + This program is free software; you can redistribute it + and/or modify it under the terms of the GNU General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later + version. + . + This program is distributed in the hope that it will be + useful, but WITHOUT ANY WARRANTY; without even the implied + warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR + PURPOSE. See the GNU General Public License for more + details. + . + You should have received a copy of the GNU General Public + License along with this package; if not, write to the Free + Software Foundation, Inc., 51 Franklin St, Fifth Floor, + Boston, MA 02110-1301 USA + . + On Debian systems, the full text of the GNU General Public + License version 2 can be found in the file + /usr/share/common-licenses/GPL-2'. diff --git a/website/public/fonts/LICENSE-IBM-Plex-Mono.txt b/website/public/fonts/LICENSE-IBM-Plex-Mono.txt new file mode 100644 index 0000000..83eac82 --- /dev/null +++ b/website/public/fonts/LICENSE-IBM-Plex-Mono.txt @@ -0,0 +1,93 @@ +Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ThinItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLight.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-ExtraLightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Light.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-LightItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Regular.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Italic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Medium.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-MediumItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-SemiBoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-Bold.ttf: Copyright 2017 IBM Corp. All rights reserved. IBMPlexMono-BoldItalic.ttf: Copyright 2017 IBM Corp. All rights reserved. + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/website/public/fonts/LICENSE-Inter.txt b/website/public/fonts/LICENSE-Inter.txt new file mode 100644 index 0000000..40589da --- /dev/null +++ b/website/public/fonts/LICENSE-Inter.txt @@ -0,0 +1,93 @@ +Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) Inter-Italic[opsz,wght].ttf: Copyright 2016 The Inter Project Authors (https://github.com/rsms/inter) + +This Font Software is licensed under the SIL Open Font License, Version 1.1. +This license is copied below, and is also available with a FAQ at: +http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/website/public/fonts/dejavu-sans-mono-diagram.woff2 b/website/public/fonts/dejavu-sans-mono-diagram.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..de6e29f14f0c5d6b2a0e948aaabac07a73ad5270 GIT binary patch literal 12380 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%0DiJ2EOCC_ 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 Ok?sOt{%}m6E@=R`r0ezo literal 0 HcmV?d00001 diff --git a/website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 b/website/public/fonts/ibm-plex-mono-latin-400-normal.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..0804aaff92260b5359330f63e05b9ac25ccabeee GIT binary patch literal 14708 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∾Na#*TBTU(!jF?9zmR-!C*+RdvGrd0&{`| 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 + + + + + SQLClean · Clean SQL, every time + + + + + + + + + + + + + + + + + +

    + +
    +
    +
    +

    Rust-powered · Free and open source · Apache-2.0

    +

    Clean SQL, every time

    +

    A fast SQL linter and formatter.

    +

    + SQLClean flags SQL that runs but is easy to get wrong, like = NULL, applies safe fixes, and + formats every file one way. It checks 10,000 files in under a second. Your team's own Rules are a few + lines of Python. +

    +
    +
    + $ pip install sqlclean + +
    + Read the docs + View on GitHub +
    +
    + +
    + +
    +
    +
    +

    Why SQL stays messy

    +
    +
    +
    +

    Valid isn't the same as correct

    +

    WHERE x = NULL runs without an error and matches no rows.

    +
    +
    +

    Style eats review time

    +

    Reviewers argue about commas and casing instead of the logic.

    +
    +
    +

    Team rules live in people's heads

    +

    "Marts read staging, not raw" holds until someone new misses it.

    +
    +
    +

    Autofixes you can't trust

    +

    A fix that changes which rows match is a bug, not a cleanup.

    +
    +
    +
    +
    + +
    +
    +
    +

    From a messy query to a clean one

    +

    Check, fix and format, then keep it that way with your own Rules, in CI and before every commit.

    +
    + +
    +

    Find the problems

    +
    +
    + 01 · Catch common mistakes +

    Flag SQL that runs but is easy to get wrong

    +

    + Default Rules flag comparisons with NULL, NOT IN lists that contain + NULL, and ROW_NUMBER with no deterministic order. Each finding points at the + exact characters and says what to write instead. +

    +

    How Rules work →

    +
    +
    sqlclean check
    +error[SQC018]: ROW_NUMBER has no deterministic ordering
    +  --> models/open_orders.sql:2:3
    +   |
    + 2 |   ROW_NUMBER() OVER (PARTITION BY o.customer_id) AS order_rank
    +   |   ^^^^^^^^^^
    +  = help: Add ORDER BY with a stable tie-breaker inside the window specification.
    +
    +error[SQC001]: Comparison with NULL is never true
    +  --> models/open_orders.sql:4:22
    +   |
    + 4 | WHERE o.cancelled_at = NULL
    +   |                      ^
    +  = help: Use IS NULL or IS NOT NULL when testing for NULL.
    +  = fix: available with `sqlclean fix --unsafe-fixes`
    +
    +error[SQC019]: NOT IN list contains NULL and can reject every row
    +  --> models/open_orders.sql:5:16
    +   |
    + 5 |   AND o.status NOT IN ('refunded', NULL)
    +   |                ^^^
    +  = help: Remove NULL from the list or express the anti-join with a NULL-safe NOT EXISTS.
    +
    +Found 3 problems (1 fixable with `sqlclean fix --unsafe-fixes`).
    +
    + +

    Fix them

    +
    +
    + 02 · Fix safely +

    Apply safe fixes, and hold back the rest

    +

    + sqlclean fix applies safe fixes until the file is stable, then formats it. A fix that can + change which rows match, like = NULL to IS NULL, waits for + --unsafe-fixes. A pass that would break parsing is thrown away. +

    +

    How fixing works →

    +
    +
    sqlclean fix --unsafe-fixes --diff
    +--- a/models/open_orders.sql
    ++++ b/models/open_orders.sql
    +@@ -1,5 +1,8 @@
    +-SELECT o.order_id, o.customer_id, o.total_cents,
    +-  ROW_NUMBER() OVER (PARTITION BY o.customer_id) AS order_rank
    ++SELECT
    ++    o.order_id,
    ++    o.customer_id,
    ++    o.total_cents,
    ++    ROW_NUMBER() OVER (PARTITION BY o.customer_id) AS order_rank
    + FROM orders o
    +-WHERE o.cancelled_at = NULL
    +-  AND o.status NOT IN ('refunded', NULL)
    ++WHERE
    ++    o.cancelled_at IS NULL AND o.status NOT IN ('refunded', NULL)
    +Would fix 1 problem(s) and change 1 file(s).
    +
    + +
    +
    + 03 · One canonical format +

    Stop arguing about layout

    +

    + Every statement gets the same layout: upper-case keywords, one column per line and each clause on its + own line. Formatting a formatted file changes nothing, and SQL the formatter can't lay out safely is reported and left as it is. +

    +

    How formatting works →

    +
    +
    sqlclean format --diff models/revenue.sql
    +--- a/models/revenue.sql
    ++++ b/models/revenue.sql
    +@@ -1,3 +1,17 @@
    +-with paid as (select order_id, sum(amount_cents) as paid_cents from payments where status = 'paid' group by order_id)
    +-select o.order_id, o.ordered_at::date as order_date, coalesce(p.paid_cents, 0) as paid_cents
    +-from orders o left join paid p on p.order_id = o.order_id
    ++WITH paid AS (
    ++    SELECT
    ++        order_id,
    ++        SUM(amount_cents) AS paid_cents
    ++    FROM payments
    ++    WHERE status = 'paid'
    ++    GROUP BY
    ++        order_id
    ++)
    ++
    ++SELECT
    ++    o.order_id,
    ++    o.ordered_at::date AS order_date,
    ++    COALESCE(p.paid_cents, 0) AS paid_cents
    ++FROM orders o
    ++LEFT JOIN paid p
    ++    ON p.order_id = o.order_id
    +1 file(s) would be reformatted, 0 file(s) already formatted.
    +
    + +

    Keep them fixed

    +
    +
    + 04 · Make lessons permanent +

    Turn review comments into Rules

    +

    + A custom Rule is a Python function that reads the tables, joins, columns and literals of each statement, + already located for you. Here marts must read staging models, never raw tables. +

    +

    Write a custom Rule →

    +
    +
    # sql_rules/layers.py
    +@rule(
    +    code="XSQCLAYER001",
    +    message="Marts must not read raw tables",
    +    remediation="Read the staging model instead.",
    +    enabled_by_default=True,
    +)
    +def marts_read_staging(*, statement: Statement, ctx: RuleContext) -> list:
    +    if not statement.path.startswith("models/marts/"):
    +        return []
    +    return [
    +        ctx.finding(subject=statement, node=table)
    +        for table in statement.tables
    +        if table.qualified_name.startswith("raw.")
    +    ]
    +
    +sqlclean check
    +error[XSQCLAYER001]: Marts must not read raw tables
    +  --> models/marts/revenue.sql:5:12
    +   |
    + 5 | INNER JOIN raw.payments p
    +   |            ^^^^^^^^^^^^
    +  = help: Read the staging model instead.
    +
    + +
    +
    + 05 · Test your Rules +

    Rules with tests, like any other code

    +

    + sqlclean.testing parses each case with the same engine as sqlclean check and + fails when the number of findings is wrong. It runs every case twice, in opposite file orders, to catch + Rules that aren't deterministic. +

    +

    Test a custom Rule →

    +
    +
    # tests/test_layers.py
    +from sqlclean.testing import RuleCase, evaluate_rule
    +
    +def test_marts_reading_raw_tables_are_reported() -> None:
    +    evaluate_rule(
    +        rule=marts_read_staging,
    +        test_case=RuleCase(
    +            description="a mart joins raw.payments",
    +            path="models/marts/revenue.sql",
    +            source="SELECT o.order_id FROM stg_orders o JOIN raw.payments p ON …",
    +            dialect="duckdb",
    +            expected_finding_count=1,
    +        ),
    +    )
    +
    +pytest tests
    +..                                                    [100%]
    +2 passed in 0.04s
    +
    + +
    +
    + 06 · Review every exception +

    Allow a finding once, with a reason

    +

    + An exception allows one Rule in one file, and for a cross join, one relation, with a reason a reviewer + can read. When the SQL changes and the exception no longer matches, sqlclean check fails + until it is removed, so allowances never outlive the code they covered. +

    +

    How exceptions work →

    +
    +
    # sqlclean.toml
    +[[rules.rule_exceptions]]
    +rule = "SQC038"
    +path = "models/marts/calendar.sql"
    +relation = "regions"
    +reason = "Every date is reported for every region."
    +
    +sqlclean check
    +error[SQC038]: CROSS JOIN with `markets` creates an explicit cartesian product
    +  --> models/marts/calendar.sql:5:1
    +   |
    + 5 | CROSS JOIN markets r
    +   | ^^^^^
    +  = help: Use a keyed JOIN, or make one side provably at most one row …
    +
    +error: stale rule exception: SQC038 in models/marts/calendar.sql for relation regions
    +  matches no finding; remove it from sqlclean.toml
    +
    + +
    +
    + 07 · Run it everywhere +

    Before every commit and in CI

    +

    + Ready-made pre-commit hooks check, fix or format staged SQL. In CI, sqlclean check exits + non-zero on findings and writes SARIF for code-scanning annotations, or JSON for anything else. +

    +

    Set up CI and pre-commit →

    +
    +
    # .pre-commit-config.yaml
    +repos:
    +  - repo: https://github.com/sqlstack-io/sqlclean
    +    rev: v0.0.1
    +    hooks:
    +      - id: sqlclean-fix
    +      - id: sqlclean-format
    +
    +sqlclean check --output-format sarif > sqlclean.sarif
    +{
    +  "version": "2.1.0",
    +  "runs": [{
    +    "tool": { "driver": { "name": "sqlclean", … } },
    +    "results": [{
    +      "ruleId": "XSQCLAYER001",
    +      "message": { "text": "Marts must not read raw tables. Read the staging model instead." },
    +      …
    +
    +
    +

    Real output from sqlclean on small example projects, trimmed to the relevant lines.

    +
    +
    + +
    +
    +
    +

    Built for big SQL repositories

    +

    The rest of what makes SQLClean easy to adopt.

    +
    +
    +
    +

    13 Rules on by default, 45 in all

    +

    The defaults cover NULLs, joins, ordering and duplicates. The other 32 cover style and structure, and you opt in to them.

    +
    +
    +

    Faster than sqruff and SQLFluff

    +

    On 10,282 files, sqlclean check takes 0.8 s with defaults; sqruff takes 41 s and SQLFluff 15 minutes. See the benchmark →

    +
    +
    +

    A dialect per folder

    +

    Set one dialect for the project and override it for any folder, such as Snowflake models beside DuckDB tests.

    +
    +
    +

    Exact locations

    +

    Findings and fixes use character offsets, so multibyte text doesn't shift an edit.

    +
    +
    +

    Text, JSON and SARIF

    +

    Readable in a terminal, structured for scripts, and annotated in code review.

    +
    +
    +

    An embeddable Rust crate

    +

    The Rules and formatter live in their own crate, so another tool can run them under its own codes.

    +
    +
    +
    +
    + +
    +
    +
    +

    Start in a minute

    +

    + Install it, point it at your SQL, and add a sqlclean.toml when you want to choose a dialect + or Rules. +

    +
      +
    • No account, server or plugins to set up.
    • +
    • Custom Rules run in the same Python environment as the command.
    • +
    • Free and open source, with no paid tier.
    • +
    +
    + DuckDB + Snowflake + BigQuery + PostgreSQL + Databricks + Redshift + ClickHouse + SQL Server + MySQL + Trino +
    +
    +
    pip install sqlclean
    +sqlclean check
    +…
    +Found 1 problem (1 fixable with `sqlclean fix --unsafe-fixes`).
    +sqlclean fix --unsafe-fixes
    +Fixed 1 problem(s) and changed 1 file(s).
    +All checks passed.
    +
    +
    + +
    +
    +

    Make clean SQL the default

    +

    The docs cover every command, every Rule, configuration and custom Rules.

    + +
    +
    +
    + + + + + + diff --git a/website/public/styles.css b/website/public/styles.css new file mode 100644 index 0000000..e8a210e --- /dev/null +++ b/website/public/styles.css @@ -0,0 +1,930 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin-wght-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); +} + +/* IBM Plex Mono has no box-drawing glyphs. DejaVu Sans Mono fills them in, and draws whole + diagrams so every cell has the same advance width. */ +@font-face { + font-family: 'SQLClean Box Drawing'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); + unicode-range: U+2500-259F, U+2260; +} + +@font-face { + font-family: 'SQLClean Diagram'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); +} + +/* Tokens shared with the docs theme in src/styles/sqlclean.css. */ +:root { + color-scheme: light; + --radius: 2px; + --background: #ffffff; + --foreground: #171a1f; + --muted-foreground: #626a76; + --faint: #98a0ad; + --primary: #0b8a74; + --primary-hover: #097562; + --primary-foreground: #ffffff; + --panel: #ffffff; + --panel-border: rgba(28, 32, 38, 0.2); + --inner-line: rgba(28, 32, 38, 0.08); + --code-bg: #fbfcfe; + --code-fg: #2b3038; + --success: #16825c; + --warning: #aa6c16; + --error: #c74757; + --stale: #7756b9; + --accent-soft: rgba(11, 138, 116, 0.08); + --font-sans: 'Inter', system-ui, sans-serif; + --font-mono: 'IBM Plex Mono', 'SQLClean Box Drawing', ui-monospace, monospace; +} + +:root[data-theme='dark'] { + color-scheme: dark; + --background: #0d0f12; + --foreground: #e9ebef; + --muted-foreground: #9ba1ac; + --faint: #626975; + --primary-foreground: #0d0f12; + --primary: #1fd8b8; + --primary-hover: #4fe3ca; + --panel: #171a1f; + --panel-border: transparent; + --inner-line: rgba(176, 184, 198, 0.1); + --code-bg: #111418; + --code-fg: #c9cdd4; + --success: #54cf8e; + --warning: #f0c267; + --error: #f47089; + --stale: #a78bfa; + --accent-soft: rgba(31, 216, 184, 0.12); +} + +*, +*::before, +*::after { + box-sizing: border-box; +} + +html { + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; +} + +body { + margin: 0; + background: var(--background); + color: var(--foreground); + font-family: var(--font-sans); + font-size: 16px; + line-height: 1.6; + -webkit-font-smoothing: antialiased; +} + +a { + color: var(--primary); + text-decoration: none; +} + +a:hover { + text-decoration: underline; +} + +code, +pre { + font-family: var(--font-mono); +} + +.wrap { + width: min(1240px, 100% - 48px); + margin: 0 auto; +} + +/* Header */ +.site-header { + position: sticky; + top: 0; + z-index: 10; + background: var(--background); + border-bottom: 1px solid var(--inner-line); +} + +.site-header .wrap { + display: flex; + align-items: center; + justify-content: space-between; + height: 60px; +} + +.brand { + display: inline-flex; + align-items: center; + gap: 10px; + color: var(--foreground); + font-weight: 650; + font-size: 18px; + letter-spacing: -0.01em; +} + +.brand:hover { + text-decoration: none; +} + +.brand img { + width: auto; + height: 30px; +} + +.brand .logo-for-dark, +:root[data-theme='dark'] .brand .logo-for-light { + display: none; +} + +:root[data-theme='dark'] .brand .logo-for-dark { + display: block; +} + +.nav { + display: flex; + align-items: center; + gap: 22px; + font-size: 14px; +} + +.nav a { + color: var(--foreground); + font-weight: 500; + opacity: 0.8; + text-decoration: underline; + text-decoration-color: transparent; + text-decoration-thickness: 2px; + text-underline-offset: 6px; + transition: + opacity 120ms, + text-decoration-color 120ms; +} + +.nav a:hover { + opacity: 1; + text-decoration-color: var(--primary); +} + +.nav .nav-icon { + display: inline-grid; + place-items: center; + color: var(--muted-foreground); +} + +.nav .nav-icon:hover { + color: var(--foreground); + opacity: 1; +} + +.nav-icon svg { + width: 18px; + height: 18px; +} + +.theme-toggle { + display: inline-grid; + place-items: center; + width: 32px; + height: 32px; + padding: 0; + border: 1px solid var(--inner-line); + border-radius: var(--radius); + background: transparent; + color: var(--muted-foreground); + cursor: pointer; +} + +.theme-toggle:hover { + color: var(--foreground); +} + +.theme-toggle svg { + width: 16px; + height: 16px; +} + +.icon-sun { + display: none; +} + +:root[data-theme='dark'] .icon-sun { + display: block; +} + +:root[data-theme='dark'] .icon-moon { + display: none; +} + +/* Hero */ +.hero { + padding: 96px 0 72px; +} + +.eyebrow { + margin: 0 0 20px; + color: var(--muted-foreground); + font-size: 14px; +} + +.hero h1 { + margin: 0; + max-width: 14ch; + font-size: clamp(44px, 7vw, 76px); + font-weight: 700; + line-height: 1.02; + letter-spacing: -0.035em; +} + +.tagline { + margin: 24px 0 0; + font-size: clamp(20px, 2.4vw, 26px); + font-weight: 550; + letter-spacing: -0.015em; +} + +.lede { + max-width: 62ch; + margin: 20px 0 0; + color: var(--muted-foreground); + font-size: 18px; +} + +.hero .lede { + max-width: 68ch; +} + +.actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 12px; + margin-top: 36px; +} + +.button { + display: inline-flex; + align-items: center; + height: 44px; + padding: 0 18px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + color: var(--foreground); + font-size: 15px; + font-weight: 550; +} + +.button:hover { + text-decoration: none; + border-color: var(--faint); +} + +.button.primary { + border-color: var(--primary); + background: var(--primary); + color: var(--primary-foreground); +} + +.button.primary:hover { + border-color: var(--primary-hover); + background: var(--primary-hover); +} + +.install { + display: inline-flex; + align-items: center; + gap: 12px; + height: 44px; + padding: 0 8px 0 16px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--code-bg); + font-family: var(--font-mono); + font-size: 14px; +} + +.install .prompt { + color: var(--faint); +} + +.copy { + height: 28px; + padding: 0 10px; + border: 1px solid var(--inner-line); + border-radius: var(--radius); + background: transparent; + color: var(--muted-foreground); + font-family: var(--font-sans); + font-size: 12px; + cursor: pointer; +} + +.copy:hover { + color: var(--foreground); +} + +/* Sections */ +section { + padding: 72px 0; + border-top: 1px solid var(--inner-line); +} + +.section-head { + max-width: 68ch; + margin-bottom: 40px; +} + +h2 { + margin: 0; + font-size: clamp(28px, 3.4vw, 38px); + font-weight: 680; + line-height: 1.15; + letter-spacing: -0.025em; +} + +.section-head p { + margin: 14px 0 0; + color: var(--muted-foreground); + font-size: 17px; +} + +h3 { + margin: 0; + font-size: 18px; + font-weight: 650; + letter-spacing: -0.01em; +} + +/* Problem */ +.problem { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 16px; +} + +.card { + padding: 24px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); +} + +.card p { + margin: 10px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +/* Walkthrough */ +.steps { + display: grid; + gap: 16px; + counter-reset: step; +} + +.step { + display: grid; + grid-template-columns: minmax(0, 4fr) minmax(0, 8fr); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + overflow: hidden; +} + +.step-text { + padding: 28px; +} + +.step-text .num { + display: block; + margin-bottom: 12px; + color: var(--primary); + font-family: var(--font-mono); + font-size: 13px; + font-weight: 500; +} + +.step-text p { + margin: 10px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +.step-text .more { + margin-top: 14px; + font-size: 14px; +} + +.terminal { + margin: 0; + padding: 22px 24px; + border-left: 1px solid var(--inner-line); + background: var(--code-bg); + color: var(--code-fg); + font-size: 12px; + line-height: 1.65; + overflow-x: auto; + white-space: pre; +} + +.terminal .cmd { + color: var(--foreground); + font-weight: 500; +} + +.terminal .cmd::before { + content: '$ '; + color: var(--faint); + font-weight: 400; +} + +.terminal .boxes { + display: block; + font-family: 'SQLClean Diagram', var(--font-mono); + line-height: 1.15; +} + +.terminal .tree { + display: block; + font-family: 'SQLClean Diagram', var(--font-mono); +} + +.terminal .dim { + color: var(--faint); +} + +.terminal .ok { + color: var(--success); +} + +.terminal .bad { + color: var(--error); +} + +.terminal .warn { + color: var(--warning); +} + +.terminal .note { + color: var(--stale); +} + +.terminal .info { + color: var(--primary); +} + +.terminal .strong { + font-weight: 500; +} + +.step-text code, +.grid code, +.caption code { + padding: 1px 4px; + border-radius: var(--radius); + background: var(--accent-soft); + color: var(--foreground); + font-size: 0.86em; + word-spacing: -0.2em; +} + +.caption { + margin: 16px 0 0; + color: var(--faint); + font-size: 13px; +} + +/* Feature grid */ +.grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); +} + +.grid .item { + padding: 24px; + border-right: 1px solid var(--inner-line); + border-bottom: 1px solid var(--inner-line); +} + +.grid .item:nth-child(3n) { + border-right: none; +} + +.grid .item:nth-last-child(-n + 3) { + border-bottom: none; +} + +.grid .item p { + margin: 8px 0 0; + color: var(--muted-foreground); + font-size: 15px; +} + +/* Start */ +.start { + display: grid; + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + gap: 40px; + align-items: start; +} + +.start ul { + margin: 20px 0 0; + padding: 0; + list-style: none; +} + +.start li { + position: relative; + padding: 10px 0 10px 22px; + border-top: 1px solid var(--inner-line); + color: var(--muted-foreground); + font-size: 15px; +} + +.start li::before { + content: ''; + position: absolute; + top: 20px; + left: 2px; + width: 7px; + height: 7px; + border-radius: 50%; + background: var(--success); +} + +.start .terminal { + border: 1px solid var(--panel-border); + border-radius: var(--radius); + white-space: pre-wrap; +} + +.adapters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-top: 28px; +} + +.adapters span { + padding: 4px 10px; + border: 1px solid var(--panel-border); + border-radius: var(--radius); + background: var(--panel); + font-size: 13px; +} + +.adapters small { + margin-left: 4px; + color: var(--muted-foreground); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; +} + +/* Closing call to action */ +.closing { + padding: 88px 0 96px; + text-align: center; +} + +.closing h2 { + max-width: 22ch; + margin: 0 auto; +} + +.closing p { + max-width: 56ch; + margin: 16px auto 0; + color: var(--muted-foreground); + font-size: 17px; +} + +.closing .actions { + justify-content: center; +} + +footer { + padding: 28px 0; + border-top: 1px solid var(--inner-line); + color: var(--faint); + font-size: 13px; +} + +footer .wrap { + display: flex; + flex-wrap: wrap; + justify-content: space-between; + gap: 12px; +} + +footer a { + color: var(--muted-foreground); +} + +@media (max-width: 880px) { + .problem, + .grid, + .start, + .step { + grid-template-columns: 1fr; + } + + .terminal { + border-left: none; + border-top: 1px solid var(--inner-line); + } + + .grid .item, + .grid .item:nth-child(3n), + .grid .item:nth-last-child(-n + 3) { + border-right: none; + border-bottom: 1px solid var(--inner-line); + } + + .grid .item:last-child { + border-bottom: none; + } + + .hero { + padding: 64px 0 48px; + } + + .nav .hide-sm { + display: none; + } +} + +/* Syntax colours for the SQL and Python examples. */ +:root { + --syn-keyword: #1a56c4; + --syn-string: #0a7a3f; + --syn-number: #9a6700; + --syn-comment: #8a909c; + --syn-function: #7c3aed; + --syn-type: #0f766e; +} + +:root[data-theme='dark'] { + --syn-keyword: #5aa9e6; + --syn-string: #79c98a; + --syn-number: #e3b341; + --syn-comment: #5c606b; + --syn-function: #c79bf0; + --syn-type: #4dd6c4; +} + +.terminal .kw { + color: var(--syn-keyword); +} + +.terminal .str { + color: var(--syn-string); +} + +.terminal .num { + color: var(--syn-number); +} + +.terminal .cm { + color: var(--syn-comment); +} + +.terminal .fn { + color: var(--syn-function); +} + +.terminal .ty { + color: var(--syn-type); +} + +.terminal .arg { + color: var(--muted-foreground); +} + +.step-group { + margin: 24px 0 0; + color: var(--muted-foreground); + font-size: 13px; + font-weight: 600; + letter-spacing: 0.01em; +} + +.step-group:first-child { + margin-top: 0; +} + +/* Hero visual: sqlclean fix turns a messy query into a clean one. */ +.hero { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + gap: 48px; + align-items: center; +} + +.hero-visual.terminal { + border: 1px solid var(--panel-border); + border-left: 1px solid var(--inner-line); + border-radius: var(--radius); + min-height: 300px; +} + +.hero-visual { + font-family: var(--font-mono); +} + +.hero-visual .tree { + display: inline; +} + +.hero-visual .hv-stack { + display: grid; +} + +.hero-visual .hv-stack > * { + grid-area: 1 / 1; +} + +.hero-visual .hv-l { + display: block; +} + +.hero-visual .hv-a, +.hero-visual .hv-b, +.hero-visual .hv-l { + animation-duration: 10s; + animation-iteration-count: infinite; + animation-fill-mode: both; + animation-timing-function: ease-out; +} + +.hero-visual .hv-a { + animation-name: hv-a; +} + +.hero-visual .hv-b { + animation-name: hv-b; +} + +@keyframes hv-a { + 0%, + 15% { + opacity: 1; + } + 18%, + 96% { + opacity: 0; + } + 100% { + opacity: 1; + } +} + +@keyframes hv-b { + 0%, + 15% { + opacity: 0; + } + 18%, + 92% { + opacity: 1; + } + 96%, + 100% { + opacity: 0; + } +} + +@keyframes hv-l0 { + 0%, + 6% { + opacity: 0; + transform: translateY(3px); + } + 9%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l0 { + animation-name: hv-l0; +} + +@keyframes hv-l1 { + 0%, + 24% { + opacity: 0; + transform: translateY(3px); + } + 27%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l1 { + animation-name: hv-l1; +} + +@keyframes hv-l2 { + 0%, + 30% { + opacity: 0; + transform: translateY(3px); + } + 33%, + 92% { + opacity: 1; + transform: none; + } + 96%, + 100% { + opacity: 0; + } +} + +.hv-l2 { + animation-name: hv-l2; +} + +@media (prefers-reduced-motion: reduce) { + .hero-visual .hv-a, + .hero-visual .hv-b, + .hero-visual .hv-l { + animation: none; + } + + .hero-visual .hv-a { + opacity: 0; + } +} + +@media (max-width: 1060px) { + .hero { + grid-template-columns: minmax(0, 1fr); + } + + .hero-visual { + justify-self: start; + } +} + +@media (max-width: 620px) { + .hero-visual { + justify-self: stretch; + } + + .hero-visual .hv-wide { + display: none; + } + + .hero-visual .hv-break::after { + content: '\\\A '; + } +} diff --git a/website/recordings/README.md b/website/recordings/README.md index f0bc2c2..0076f23 100644 --- a/website/recordings/README.md +++ b/website/recordings/README.md @@ -20,7 +20,7 @@ The README uses `check-fix`, copied into `.github/demos/`. ## Record ```bash -cd docs/recordings +cd website/recordings ./record.sh check-fix # out/check-fix.gif, out/check-fix.mp4 and out/check-fix.png cp out/check-fix.gif ../../.github/demos/ ``` diff --git a/website/scripts/check_links.py b/website/scripts/check_links.py new file mode 100644 index 0000000..1cb1e88 --- /dev/null +++ b/website/scripts/check_links.py @@ -0,0 +1,65 @@ +"""Fail when a built page links to a missing page or anchor on the same site.""" + +import sys +from html.parser import HTMLParser +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +class PageParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.hrefs: list[str] = [] + self.ids: set[str] = set() + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + values = dict(attrs) + if values.get("id"): + self.ids.add(values["id"] or "") + if tag == "a" and values.get("href"): + self.hrefs.append(values["href"] or "") + + +def route_for(dist: Path, file: Path) -> str: + relative = "/" + file.relative_to(dist).as_posix() + return relative.removesuffix("index.html") + + +def main() -> int: + dist = Path(sys.argv[1] if len(sys.argv) > 1 else "dist") + pages: dict[str, PageParser] = {} + for file in dist.rglob("*.html"): + parser = PageParser() + parser.feed(file.read_text()) + pages[route_for(dist, file)] = parser + + broken: list[str] = [] + for route, page in sorted(pages.items()): + if route.startswith("/404"): + continue + for href in page.hrefs: + url = urlsplit(href) + if url.scheme or url.netloc: + continue + path = unquote(url.path) or route + if not path.startswith("/"): + path = route + path + if not path.endswith("/") and "." not in path.rsplit("/", 1)[-1]: + path += "/" + target = pages.get(path) + if target is None: + if not (dist / path.lstrip("/")).exists(): + broken.append(f"{route}: missing page {href}") + continue + fragment = unquote(url.fragment) + if fragment and fragment != "_top" and fragment not in target.ids: + broken.append(f"{route}: missing anchor {href}") + + for line in broken: + print(line) + print(f"{len(pages)} pages, {len(broken)} broken links") + return 1 if broken else 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/website/src/assets/logo-dark.png b/website/src/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9fecb46a0ae81ceb912e2c91472e7d09904cbc20 GIT binary patch literal 19315 zcmZ^~1xzJQ4>*XsJ1p+*?yigbg9i^-+!uFucXxMp_r=|PfyLeZ|NC;eOD>noB%Mj8 zP20&#GwpODRFq_p5eN{#z`&5@WF^(Wz`*JL8;ioh{0F761B(CaNKItb6v4neslmYf zgTcVw{zLhnfPuNNf`OfZz`*#^!N71GvO84;|C@j@R*;bd`~F|c?i-vmt^F@cGmrm&hDS1fwEq}r{~rm?mbRu&&PKKl|6d*sw*O1W;+NVo7?`}e zoTQkB`^IICTeg8_%X6P~-o4=L!$U9{1_Wg@7=kf0cvs=7wrYd%*iL=^j`hIro>pt^ zhm8@0$jEY1ZY->1didan+<1PDjJsr~mFv%g?6T>ftjVZ4W+5C?g0q%hmo2LXUNyUb z%@PpAtRbTPRZ zIvN;kXn;{f4~zu+rSt`ut67*U{JHYfGmdpCh%VBEGU0D~jGAe(=+C(6_P%v4xugdc z1=m@7$Lj=25Zao&=`XU8SPY!TYYeK9a0`@CG8h@sSTMa{Hq>uc%n9G&FGfcS8bSK*n!CK<5%P(1fr4U#ipGIz;~!Z#v-_m|Lf}zwX6|PkjBAin7wG*F{i^-?`C&g>n%hKUlsB>GUs_wM zDxREbE|I|CNvYPr>ZL63Z)k>>?K*|Xk>CRev5d$@7<>EH`Tu8x*mGB6V%ZWSXksdF z*qVq;5|#79d5BoKH2rO~yW;AwuA#=7p2%8N_R*eN&CG?1Cm~RM(_R32b@G8jqEAUv z4_ozt02?FHSr@x$<~?iVf9ftd_t8~kN&@f;G|mQr#c~8Pj?PxBUeKdOqnB#o}aBTsC1WD5Q3XCyU>K)q`X8}Hn@1>tY_J*sm#|EEu*n=V=4~mQgZ25u^ zIor=&^Z(h&Dfg~Wn>AO(A=6RjTioT#G=9V+viwr@s%$I6(2~dT^bzrGIrTZ3Ti4ph zH6TReDGsyQ}}VhJ;h<=NH5N7^Ie686~V{@LVgNmfp-m)tVvYN5}4APdP!nF&}-c zPS=7MYq0}XJ_^?Q$zr9VW=IH+Cg52D5%PJ(a-9Osd3tIn&fC58?-a{*=&O@#!Hd0w zKkQq6Ayg_6+&9@N#0Uko&W_O$#t8LoBd%0dj{T5Ez3`rJcsLi zZZQV*0jcu^o8c{ifm(sJU8r8x|Czm;oQy>FXwE?SJT~6aNYz*-t#4}sr7Zu9ewsyF zNOP)0?u{w)2rvmVq42;Q!2n(f4|K^dTyK3k$T9jcSR_4ViO8^EbF>5&dE+On%vF>C z8|?WwuX(5T$oepwFlzp%yvS5O)(ZxD% zogL}?Y_Q0sWn|vvQ*OrM zjaEN_SwUB)buRb@6J{wKgD}M#Z2rbsr&L_%gV+|)t;fKM0y&OhD(8PxS%-!Hy|M-6 zVSP#L!Gpe0F9y=5i6JHM;`qa;IYpNePNuOA1%9kvvId*E8vX;a*Iy7LOPMwcjs*ya z`VG7^L`?>+6Ga|eP%DWIK&a?Co+tYB4p#r75NHn(*1DC8Cbn{d-A@^uK`K`c1}~Tw zu46q@xK3lv`fscBoe0k^>vU=mdW2(kLQv;2Xw*=8`EIGEU!}~WN9F+W;@vwKg8!<9 z<`}OPiH-#j`O|J&V%1@H&Jny?3)P+4v~?xpCgpHL*np?XzX>^%9JYm_#n8*ia#vFd zKhTy(|7rNtFA{GJI_s?f_G0S&efq28RJg6URu@7c{7>Km zV?D^83A>cZ@ik382k6F2XIT>{6@p%Wj@-4=^EQH37^K1ra6lTJ4`qV*7g)OIY>c%* z3T?tv&MidQc(Kc7*nD0^kRiSiv<6X~tb@=wGIx2kfm2wXjqND6PsaA-FH`8|JCc0% zN9DqkrgRbc6TCn2GV;cvW=~6+6+%FQ1*m}=3o@r!OiNbjR$DWCk?xh^HxvW{ zPgJ78xM`1XL*A0=0F-Jd7Ic~Cc~_-Ok`7UJr>A|W{>0K03qVV|YH|BAFY2l;E}jLG z8TeCI(2Wxb)Vbyv&2GY)5beuvgI(yM45^^p5&PIyR#6m|zYP%$l})!?>zcl}P3)zA zm~5YGK^7}a7HY})$?%f|TFba~VOW`AD9EuuCEGY;0SUs%A2J{ai3p7QM|$8%(g|mg zB+O)FIb_C^T9LtFEPXxh3G5?FEw{?BdkR#BUStO?sOr@oGT` zWzT|`(#8d{sMvbY?tnY#nnB|+tIvuRSm_lpd9EuFWF=@tCZga9;;2kB+z{ck&>u=` za)cL-9F77l?jwQgadX=wahR*kkM0~wBm)%C4rci`!Su0{|6Zd=gY~bxwC91{b;=4GQ+mp=ptBOj2!Y1Cd+G%2N}SM9wV z_i{HhyXVs7X53`f(nf|3DHo{ktaLgq5?m-=YKV;B2zGW|)*+y#vn6XV+Hcm1;9Uq?-uPFuw3{}1NnpH-N9GdV z=au2f=lvx9=bEka)326YQYWuAVX6N9G?D&S^S&%Mi1r=%`nZ3F1S;rnKSq_&`efzo zTIHzf>y7ZH)u=y3J;0pM@y%w_YiEek6p-ohmNi5C!NUJbBJBqbUZ0d#j zoBJnRME<^nj5&C3kkm$*2@mh5(CshzW8XhpDU6~Q#OLouC8Xh1MiQ(+Ua6Lq$-~Q$ zjzkoSo>`j1?elVYQT}z`1A!3GjF$YjWzV7ghA#L!Sdhssq*Z>%!6l%(TnySM-Q%-( zvKwt%O~_1_yu9xIl0uU6g4hq}r|WkF!6q#uApXJ}C63_2;{nqyGw9+%37OYG)8#HI z>4Kr1$&O?eMD^squ^`@09?M_YlF7Io-m>K7c4~roqh9;Hw)2$hmJ%A)wy2pcRhdj1 zTVAn%u6Iit6o%*(j~@oMcz1jyWi$0sY8`Bc5d4*FGkQqD5r+VuVaCy>wQT5H9Ek#ZaidC?#Mm! zuUy7&eo;+{mp^9R{DEcgmVYTD4svs{=&4jU?)=75wiz7976}Z9N*mp1#{0o?lW#dK z^{?-$dpl?nygdE8L`JuF8D5XBzM3zOg%&C?a?dC`w8lR_EfZ%Ff+rxhX9TyE=2Y^T}0G27^heC7HaU!$z#Od}~5_JvN8SVT#D zjdoakGxZ^v98#+=mCIMC-KToZlic7~`hjX$*8ZS#H1h3o@-L>wt-0e`~*; zR;$!ri_}V@gyhW1x%I2{_%A1x5(LyD8AhC>4z6C^*10kY7kH(*y@p6`n^*R8XOxZ< z^uH)O@+h9||F{(CLq3A<)s~g)`s!7)^ij)iqdGm0X@yoLC|p1b-%Zxd7ibceVLP^I z3Ag4ii^Q~;`G{F_W3|`;KWQz2wZ$tn3>KH2fh)C9Ev`+LLz{}{be2a#XJhg%;g#1T z)=HV4ar*XY*5|xn)+X_7&f#8@WODLU+lN;ya!FQcf3QKxOtRMft%mw`mWjeYYom?` zIxtI?0%e`Jz#7+IgP46bQ;2CI08&eo4?W-FeQzWW1!)^6ZGEO!Z!B^xjwc-#$KeXf zmCu*J5=!)z0`!STIo?O9tGC3mjmvSok!JgJha73(TSHe~x&G6K49Vmn+oL+wWh%q; zY!AIlUV}*&t@f8s0nK}xXr2@6Z7$Qlqhbr95FP}jS;%{*=?mag_Ur0^wxNT>>%#RW zV^dn@?E}30JbV#mKdSzr>Uo=qI*4GtO&ZUhpLKh0z(30ds!2%FCagB5UwTe@SPR6n z<{wQKM3zr3%ak-H*fL7m)Q*_1gefE%2Z}V}5OXRea=!D9hVB=G$;e~y`+mDq+Y6=t z^Bj8Y$GQK0T`#ig+w)fZ_drV~q`G?7L6}y)n%{x%c0J!*;sh}sko&i6OHqisj>a&n zRJwEfbmS^PL`oYd-ROaQn{6UDt)NQRjd?IFYTY~I-Y#uT*^HtW#6NWaZsv+2+4tzy z<1PWM@$%Xxo9BRYx|=%IluGER6@DQI5@Xl}*l$UjJ^qoYVtgPFBpJLFx39A`yuTk5 z3yV1RMq~D%IV$Vc|Dk}|CS`JDi9|mGAaT@wS=m(vRH0q3DQ*d&b||%i@}QJ7yi`Z| z2~8o_VE`dISPr#GOjt#ZRd&S)sxn3eV(s7Hxbk{rkf!|E@DC(FMS=H}A3mHulf zuJgc3M3NId5%lv~M;?UJ)Qy9xdwPqFXpX;6hE5;4^LM9oZ)_@l*&Mm*6Sb}Ejb2hS z3*R$F_=>^t6=vhr@FZW3nZ`{-5@TyXv1h^YEgag)(47Uu9K!D&;P|Ji^=HZ` zukBiJgL&U8t47;}Db>t2h1|_5!`?WHSjzEigZTb({C!#P7y86y47F>uqD)0@bPY^C z_7~~xbgW0yhCn|)WS{+mi;ffay&KklnQSS4u3#t~9KZ}XAc66sg(EoYGyx`4cZGuWnkSfvz4+p5 z2|gOvRFKt37i>)>6^V!xQ3i0X%(|SNUp`K%^RlXCrsSN8boK>0ncTT@3M|2jKR~zW zNY6w;ig{ve;RN{U?NPN|Hhe3V8b$t%0x6SgGe%&4UlFfXJ^_|(uI7)2Xuut@f<6Y! zwo}Z$b6%&&=#?;#i*RpWP8eUJT0%CNdBU4sI$ByKH*V`VsTFQnc5a8JdT zE-6%B!ea56+wg;XkZ$Xy)jQ%PgfHDGb#&tZXY@!ve82Ipl8f zjir`l38pm`;=9KnPuw%3sPjBVfC>4e;{-~LVpSS_Qyn!QTd0e^itKgIuN2d zjmEt}=;K%0!K^6>wLoXA(rbL6@3grHgU^kvR4?%O7kOdo$Sgab4w)Jr2Cl_L1)F>FG0PM=-~Dg zOX-|O{#Tr{R>)0$g{{VP(9|naT=gH6u-<}AKn-@nT0-jDH_b@JQP zuWM94zq2OuenmId908pZ23>R%AGMf4KLnJBvr2Hp)U0Ht%%+BXOO^*+zha1&R)1s& zUV*2wt|u`r?n_l(2f(cu3{iXZoPN`xMuMY(?eQoJt~qo0p|GHpK?DNMH2OX#hZpf^ z1qLqF(SQh!uWPB}6obq1gDSDJ#5#ZBAY#A)S}b}!8^*PV#m5vi&MT+K{Nfr_g0aszyM&qr?J@7>l$6h5Y?ZyE*onso6ng3+dL1cs->7s ze@bBAa$&||i?_!hdJBn(S+V8dgLc%?FRdc=%& zFGe>^p(r53Z`Qo1=O*((tG7jI!$qEJrP8> z!e{y|)L<4mF{B|6#1cOQL54Q41I4sYOn2mV&1mMnqqH4PFh!D#U1h8j^{Vn+l=Ae_ zvaRnzS(<(5a{i^;?pRzD+89PwwmdDmFAoXVrh~{5*F}&O>00Hoo1C8SqDn=sWiK|C ze|nUksMk1H>wMUZ?HF5_Ny=L6%yI53G}p*29coKt)LfI-dJtY~)zf3DB zeHp3%05+rdz{nB#RPn;dBk%&dMKH=Mi&JSghL@XRlZb_GfoI8yng+?q6 z%FHM%9}ic9aWklwLMu9HyFT*6*7li$$8jWAk;YzRtX#(l{?{IAUG6gahyFi&Q7O9i zhsOqmld*7n;%YFQ!0!pnLaZ6VuHA_gP|icGT9{0lTWT=0ZV=oDty$vQFAi%wg4s@` z1^F5mbU~=#W_T;CB2%=GS)GWtM~S@#+dW^QGmx=r?Su0$w`s4FH}vf#k|l6kxPLBA zaV{#tslqMspfg9vwn0lE^$L)7`v=3woXdR})u0rK|KLw7-2My%?;Q|4DaR)1!>@>K zS@Y1^L52QUNY82`rHm{#kV8>1OYzGrdEr^5ttS1mwnh^%0`X2RjPfU8M<`%$&{M_Dwf1`T8uMB`=>RDBnD+erW?a=Nx=Cw=ECf)GY&mNYtFlM_UvSgeQgajd zp`-UFu5hKs_HSXMQ`7ZIZA^=+t?K!TG@xss`EQAC`d|J=d3d*=-4=qZ<{0@JwfKiu zm{OX@3)!s{PkL?5TKnQF8Eva0@t!<2H;g(q{^pQol*MuhVc{2J=PPyFld|22txu+j ziA#6a>*kWJ*s?^|_K&Hyf9&=5EIb^=WJ@f*;17PEF*abKUr%MSS9Ph@Dz(MXg$o-$ zypB8@DY?J@i5C+FvJyKTCrVQaIr{Cxw&WN$ck_(7zYcA;EN9Icq?P)1E1cM;6+ZfP zDfl=RaXVJEHhPcL&122M=1DYKJB`i`D^KrpyTA=D-)%(Sr26%86Fl<&m}BP{3C7lo1Ql=Dmu(ejrafL&n|)^~JEBa8jMab6K3e zn`ZHBAM?q@v6+`;sjq)=$#lY@wVx*<>S6^LbagLgNiKhr1otr9= zQi7yfmGi`BjT&P5%@ual4?~%=YwWRm_*|Z^jv8fr*Hr=QE^e$8O|epm$ ztXs}`ZD!f;k}IZ{vJd-`#FdVBDQ3T}E)iiDk@7^XgDLy?#3gBt>V>YnSYdY@2rI}S zqTio7G5nyNL36hb3^yO{3aXIIV5K0vG1?ewN>@ADBw5OoV!hg^L;2*^y_3u&74p~b z=;({qi=Kx(WGXI$*sH|E$}6;^%SuSYT{1eCS?sQlO-+{LH9?slc3oo!VqMQmDNA%F z$xE8K)EurXo4=N++rxb0F{O`#~_bFDb#aVjpeG*rz z6DjnLM@h>{a@I#1#Y-|jd^T+6#x8;8wD@pmH|_q0dSZaIdMUKfWI;B2S`0xyqO)7lsE5&x66t=E>Kj@dE3m}gz3HG@12>#xHM~`tr!E-CT!TdD`{rCeS>w%#mc%qM9oC^21 z1YN`X0INKXP??YK?&tSWVG31CzB~~By9$(&11VnkZp@==1}n{+@%WJ(Va#TSma|yE zadqlfm6WYlX~>8p&U_ZHM$#kJe>T?l=RxmEy)_LmOUgMU4HVYtdqkyIpV-y;30PT3bZ1B;@+Om2r7^_ z_C4{aI%RomB-?gpPQux07EjJD$-x}W>9F_UtV1YoMB}VmJJS$fa*y=UWdFybQ?+ zv=XzfYWCzkG=qx69Cc7TOO~S&jwmVWeEVaHe0nH-eenCquU^eSk1Kp2647WB>0*jQ zkC;K{ff59cv>mAyRt@%-h^dD?^&f%i>YW}yNBg?XYcDktVKj-vB}Cjt{ZD_oizNYs z9qftG#&*}Mqy}TGR6gf;aYarRGO6T#2i`Z!HMPWt;tb+SUEk%lnJbe8Lr2i&lrQar zAV;rQ=p-iV*woDJ-JEmo3qh1!Pwt6SRCTM{@uN5ibdU5M$J7O;*^_y_OS_%NChne<)3Cx>D*T8sff)4)f$b*!;xD<0XBmkinkSIrep5q#=QI>*WEMMiG=5%=%2%;;j(iLK`<@2htQ>7D z=o|oEx1w0wrh*D!25eQO`MA^ zcxOSd;-3?%fj=$juTkcLXPv!gL==PKGh6M!l}*;+t9fwg)GwH+R))uWf9oc%eGJs1 zB_D`H`Y^%pv!g0a4a1S7Gjq0g>NU}tZsKRNpFzmlss>N+;^qziW>(qW8B`&e*^nx2UECYJXkck$RSnj~DUhDVu}2Wj}C436LM>g;2$* zh~V0(xc#s*xPwqVvg#;wZKsO{2}W~8<`c5a81s)!Y>lq;)w-HM;iiM9eGwGCwRQWl zmt^xrkG(;Ys^Q~M#>$04n+@{JnEhN4jX}j1F5SpFl6dRcP%D*IM$UZ-@{VzxD7quN zENGWVa=dMSyIwqwcaDWBQ9oNok8^l17XxT(tN*g;h3D!6;vDY|yr6-}g)~3*O@^?K zPEY|`pirIy8Ci|di16YS@TA*(o`vnD%gwY+krqr@M(_jCZ0*)ep1uZ0O+BR*yCCsx z!LcP3P=~*|>=#F0f~P;d!p-9i9X;{3n|0kc^WLRFq_kn$Dre;{QRF~4+JHQ1_!hHD zp9T+yMN8~K@4BZ7VlA>p0LtVf%4uP7%Xvf79@m_(HH{^&Ze!v0Jz*i@diw)=aS#)_ zf<_twZEiA(>bZjZ!XfxBfI z`!*KA(e?*he9qbEIUd*@M;xF++oX6i{)>cx@faF-y5^5ctiL@N?ZmN;P4a*tL4!xTo zV|0_hc@R;os*U>?0e@<1Y&B}mhk2XPF;1CMhnV!TBoXH7aCQjHrn#Oz{l-u(|F9u* zNC*w<(GQXkT)+6{MAr}$ucAHuDem6wq%`As7x@f&w#89pb{7L=I8E;8!NLk#;wh~y z*DEsg_o{LP_IfNjc_ezyvU-%HN8hJ-nbYjUtH0l4>L)Q9IY>31)EaWqg~nSRf;w(r zw%G05SW&=GH=cje7VIL!sVR)f6#u=Uu0n8(j}Ag2-p4X;ShEmyj6}np0rB?K@`{U) zVj)mq&ZQi|N@R|o4PJ&%qdc0c?Fp(zFcXQzLzM$ zCC89yX14E3ZS!*kRzOuE*S-+OkQ$U2c7_$AV~4PaNJ+QaLnxcXjxv8bBnobDg&VD^ zl2ftq!+hkJZF-$7Id7d1f8dAT%?s|rf-EcwS5q9Ud+VHAyf2%eK5&<(M1h&6B=UM& ze#-@f(2BTbI?6r?erS0X0y-P5M)k0uTlIuy8B2waPQcCSzwR&D zH|MX%PpB??#xM?=)6^N!_mmf?=o#2oEod;36hNyUInOOS%!J35T*EIwjDF}Cb0xPzSBH4&x+Z!}Z|8NX+i@73U9%zH*mh;nt1)MODN9!YPRjQ`rACL>#UI zml>jKIq0|@3(T^=nc_T}Bx5xmeJC}rIkMqRE@#D6{x6^k*DVUo`^~Q#ROOXl5aupo}Yr=~>P0lgOkC$~>5Z6E4;3FfM}k2pscUA14ZP%j4E?75f(J=4)# zTbm0uL0@`QvTBF1Azrx}+VgO1Jt}n}%Yn&mjFJUtK}@0@G;mT-UMb_bGOc|@<_jiP z(SzQfAUvqZUm>LD{q6Xw1#@J0d;a3Eo`^4CiZ^*H3A6Yw+5gVKUivG+;)lEN3?88n-o0#G_l#h!puTnc8c+)YP zzFH`6-E7Nx2Xv5(zYx~NqmU0#n!sDioMP-1?GsV<%8=pJ3@w)@)1XYhyw=dkShe3) z)w1ES67~FTZ^+cyOz>QU4HM$2c`_BAULI+oIV_{4am1!Rzf}&7(`)LD*q$Gep~aeN zXw3M&wP|+cV6BpFJV8;%krzvXL_LO2NKt$Qy~^3i4wwz=ix~ae6z^QIiGSD+R}2Tj zLu}IgE2~Lua_YdG@KYx|J+^;s*{i%iOj3=MJI&mO8eyx~gLB?~Sshs!YA|}E9b9yO z-;4dB2)xCgLRgx#YAyWY@Q|ab{T^!kk*H4Mj?upH3_}pL)r**{^(4*~2yh`qAYx!s zqLS!LVXHGy~l7(vintiJN;~2Hzc3DTNeTeluT5Ivc&{YAquvDIWj#R~Z0-!4lB6^KA9IV>vW#TrFh z+Ed)YWmfFrs}U;GaTymh#ao=!aqkAfcAhRBF&#!NS%+9&E=^?kV{%g~M12*A@{~fJ za>-TL7P*%};&fqfozG}!-;}VGFG%x4;EfnLaI}Z&NvlABqx6_ep2{S~OWyWgy3pgE zEeZx182~pKK+;LW5hyj=fAQIFHg8qJ3GEk~5eZrar{RXguJMq>n!30Mdm{Ys60qBM z@%1!xsGFe08ebiJJ!8jJ_7bcC#W!?h=<+4+=KNQ(Q-6+{wuxjd)}f3>QT461{iKL6 z4paZidJf-Dgmh^jqmOyJM1N~Ry3Pg1;f3Yqyma^$WHg&bd$clfVD{cT0;Tl=%5Zo9 zo@e!VH9_kdImZXAtEYdiR?TS|hREb3c~RB0_Kv;JXjP$)mBirv*j(kQ z8LIXPU+K8COqp9pHt)ktWjY9co1#!VqtakEWoxAa!pK0%P~@7gk*@v2tHJ0b(lMzA zUkkOwwO|w8W&!)@K__~r4!JE1m*NrQ(4MKuY+KANF3Ql?Y%_^{>a zN8cvIGjqNyd?!SYWve#6JX-weBJxvjlL@RL;fpkM5(9x`GL@rv+QPu8_*gg??!$A4 z2QMIb=xTvmfZQq=Sx`hT}v3xTS z=R`fdjjyZP48oljTm9RRaN1_@qesZ|+~}vrKesX7tRF#5T#*S7SpT}~FMp;>%w8O1 zmb#)2GnFhIb#9RB1vOCS#i8&Z&fX8)!PcIi%ab%#iX;Wc`N4T@`dub{1NvZ4gp->O zB}2e^)YA=iBz})aMJT%ioZMG%=VSa!7>I5;A8^{ZUW|${3^`B39+L=ir#lNL5>smu zL~C2ouIkIi!H8#3LQWW?l_sk=6h6RCAuU~;HMizdS{2G0%cmCNKFl+GZUdp84U&!hWEIj12aKxg{)<-6OqzgxD}MNh zt{H)BBblWk{*eRmMxXiH{a3v8$CJBWKl|p;ScBQb|Jgizc(Cb|#!D*y`pH&^pRP#Z z^qA|l23cz0gMHKK<{iZ#{6XfLKjxnc(me`YsTUoBTy-AN{kpRYWZ=JLS%Ry>-UkP- zoRG2O(AVl?2cr-2O~Oc2BA3=h*y1SbVE)7zO6jToT-fD3+ZsbR&b1uP5an4jpT6Kr z>}5KTA4pYWA}}2{0Wa5l!a*3h3i}5hS?w`Fk?5j6w`Ni*#^*Qs5@f2A%T2Y_--%D7 z8{P}~MzDZwXT$#Bi0De`Ad4uYhrC2>+VrUuQ71K0i>~AY8)BmT(5ah=e5cPxmzGy8 zp(I*KF11INB~pF*nfjCV5I=@beroj+Z==U^CRta6`#_eUw8lWm{&pj*pJg(tbm8_< z*>P0w8}q5480NqHTvrYqMp@1~u=efGJuzbN;giUB>CM10T^bGm97j#dk_>hk%C^_n z#dhCPhH*@G{NqnU#uaQx=-q~*sS+@VuXY;`ZFNh{B_?ty+zLPbTjT>O zF-uDK8uhNH=<%b@Eask{wHG|@`L_Q%Lf!0&(cE%(x5fLK>A3yjRbK5pei)mNHh;`s zh;gBE7pKL}M9z9O^=_-vEMu4Pnt5fTrD3Ip&SE=L$9h!;;Nm!E8#W4f{Lrb=!f7_M zRkdElYPM4pv(_F1wDHf(05L89qdfnQ^80@8+#I?FmZ?wsrG~|a0O)Dd^Kdzzn98-e z`)9JUvw3Bk#p!vrYZdJ~M?_a}e^z2Yo=li?W@^5`2{L+i{$}Uym_LjD$r)94;#?^W? zWV+@5z~sF|j0vSkC>3(YN4MaoX@UN#XKFzDA(PJjmGaJg6`~@B0FOP{_TvxfXKwX0 z?@iBUUp}uVltN0==ejyhW#`bY6}gSGD|=b#uukjGqIdbI*Ca3g$w};mOq_9-E}GxU zXQ$Msi^VpP3I;b2E3J5pE%T}?TX!@Il%_+M0yi3n3z@hYOhv22zluP)ZwHi@sbUm(Dv;Akha4KpgNj#ZbD$A357+6pZj;%HCcr;sUk#aBXS^ukx2fP{e zm{YYp)8(1&YS!I;@yn%@3g0MOHfyyjNXfN0(_QOfINDK}+0&7coBm6mYi~6ZUf$Q8 zLvPc1n23aN0ZV>vU{cR&wY?xjauaTAwg1))!)>mW83QJDwbhu8=gzyd`!(W?9Nh=F zIUoXc3amoX@LYq%5Xj<9SnpgyxERdM)zgZ=P7!2x)dTssoMX{bkQC05E!@bLa;|?D z=A)~+Edg@xJ?)bJ|^P=KEW3+>3SxVZ4WTg?Iexhng^Qf4e!R>l4 zD-x$`$n(pcXvX(`OnrQ}@%$Hv9wTd-#Tf30p7Q4Pi*>a-W!a_t#doMTnvKV=Xe8ao za>69i!Xl}gU2NZ69P*ncfX3P6X#c`MC!lxP}#nN$~ z8md6Pia-Gf9#oihXZ1_TjR<3I_gXf;jAEii<7sfzu@Cb0Gfo{Q)U8F}ceQu__Gq8V6Kc;^rN(c$Do?!PCVCo>*)sBe*u9 z6;O_VzPvdjQd27lKb3J3zV7_y-3%gqzl(jspv zKvIlM)}9s0I~_Xf*9TXmKvJrkh3;PZI6RSpAG*xMbiXq zFb*?Dws4JI5RRPO7_}j>t2+Jy($%y zX*lFBQxeL1b;Zsqe7q&@N~AxbtGMbaVXacys`jsu$hVQcu9*K~n|>6brQ$6CkUO{H zY^?BQbTJDR5L@8-HG>)+=-oEtV1z?9Vzj zhO|pBcoiBOf#^3Ygl?sh{XT@P%(HpeP2KlCSJruux{1>lyjP&8ZPmqTa_zH(@H1|I ziwlCfT)oq0yIaMVCCIi9IPnC69PfPqD(&bnjC z1XaSFDd9i9Nf2^)Mymf zMIMntK^w3ues-w$RHHPqcRo9jv5uv^1(4u#yrI2hfyFq4#FaLL_9C3keC6*m$O7d3 zc^oSX?EhA7M5i0R<7l_rZLyVo;OOejrlBa_d+X`LIl<&3~zwXuzh<1NFX`MCbDEv=; zR94kGa4DT*`Kud;qZ9>pQTIAoNl-#nM#jo)7sd}f!B;7N>$k1wl$fAwDL*lfwSU><}Oh$pYZIxwJOC^Zd< zNEnzhltDI0997oQ*^n>ROKkBfEaIm94zj9wSu(XDOmAF*G4j+T#&DH?oY9lfUJ3?{ zyfawe=u2<*QjeYEM1~XSEmPU9qA!kz(TFks_FkI7!{p1nn!5?WwT?>V@+ScV9qM)o z2W4P0s_@6PU5K!$O@>IQta$djp2I{5Z(W!$Yu)-!p#Vof1)h4b&8CwC75qgwtjEWK z8%T96@;s;CRkT)?8cQ;4QvP^c&P3nJoC z48!NYAyOt&-dwM!*jfof#*}r8?A^LG7s+hs!#JK zc8Xz}(g3uKXlsMIdJrprTI{K}!Ml|J<-Hg4>(){T4;mn6B zSmo2oE>Jh?-j{u0_~ROg0`SFNn$R-1nTu7kpG^xmK0+9nMq4;%dnae8tv}q8agiB$ zfi=rEIOgSC;X>g@y$DMAl`OtB&iZQ{?2h8UvSX04jB|_5UxH(PB5RaZ{#Cu4qZ;yE zU+MF?O~zAfv!|%}%PRMU2ci!?VK~`1T<-yp!Igp)iTJdP0vchTMDcaj<*EHs@Jk8Z z6U-38+?F$0llre3mXzx@T!DT5f>k`AfIfvv4kXb*qS-aN97J6rc)QU3_w~9}CjT+m z6b_^eL{tI_RF<9DYXkvQE1X`ebCMtsr?6e5Y6C^ziRxjxKb276%7b7%CQbv6<2c?H zEBPl`FhxQR$RT^c8zgODZ$W@x*)vll-V0^v>eF;=m;BzxWGFT zd-gt^Lqh&DYGY(L77>5_wGbbJWJD7Cy(W1I?(HlcyMv`}mUFSj{ zJVcfaMwrtwUV2!_@4v=)!Tpm*wpUPx^X~-0tqSo7KmV;Y%Lwrsmh9rXuMvcjfHy zU6IE_rdiq)U7zZ|KPI|XG5)G!-~McbdYg7xin{AeoQkA40YdfbBmk;SpUGWKwcalP zW1w}aF{;Owgu#z!WjKV6ew)+>8^nDp-<#6%yI?1}r)i(G0WXX3qd(9;({~V`0=B(e z)#0`u9zHoSN;MhQmVUxVZm{TFrM6Yh%G){+q!Sqpm&8;VS2;=kZ)= z?>eJ%ag3@zwpsR*s(`&n`uI5dEllNiAVaZUu+YcO7#!tHU_($9R{=~v9BKVe126-;>Q z6-Z;M1v=r=fjVDtAoS$cFlLw;&2@l&$LUqNTC;V611>;4yfS#BEU5O|1`x!MD%??r)m8~#sI?T)B);8Mi+*C4b%FjX|{>wmF&;KQx9kgj%^|CyqaN6;%R0wDF;PjV#;b zLy~M|##o_BWTs=VAamgo8x<`*XRfLO_5x@HpRs0)JemJIRfe#$_!4`7@O#29vG-$0 zGWR&SjgxZPgneH4j<5yDYF&*=)8!HK^;uu6^+C{S{uh>5AzpFdLa|@npKKs)>$C9W z2W(ll4W>NNep6tGo4-(mjoFkjSq9Z3T|LVZjJn|O$o~`C3MKVj(M-Ae6-7y1$I5*M z!5(Ge8Dx252hxgU80W7*V{EO~=pN&mp1D~2fp;gKoKF;|pWgFV-ac&xJq(qG=zfP0 zULLXvn=TrKNb6C&dO4QZv2}*O=UA1lT4v>Er1)DxR`HU$zp6#YT3uaRevcHH@UKUV zeALxd!>+^aS@(Q=?I$hOQYuuKy=ywtMh~Vibr>rZ2vJ02AxqD4VdW7=dGtpHA0ve@wNSsr@t^p;X$G_tcRr)lx);bEpH!Z7ghNSnMqh(X$0?MhxW&HgoEJ zwW+@7-(7<#!GQ#eqD&~|BuOAmMyTx-3NZ&s0li{mswRx1wkfssSrrmk5LscqJ zslZ<@p?d1`iE^)kLN=eL7>Nzh%~V#hg2i_DzVEgB3aW)cM#CfMEzCesjGGZuA=OoKen_?|(r~(I(CB+{MEE~t~37@}dQ`YZ&v$A{icEhq| z*fGcKhmu;l6jn`xryPhPsA?z_t$Qk>W@dXOIM?9)G&4{=87x@TeE>v7nuFZI_qc^Y z{=*STv@oO9(>PNufZHbivQW+Ee6|RE3JlXM@@evJq% z6)Y)J@eX3E5iHK45UK%1LfAmCgDG!b%jWOAjzix3BP?V-l~y|@i^>MJZb_JXU<~3v zG2SWkHA$Ze7S=j2GP;BA($p*o-XKvL1+zIO78$lgaI9s1l=)=a9`%6oQY<#k+G+%` z0$U`#D;XNNAnu!%TypWHob&W5YdM7?15ZrhLlHLJ2z4-&H*pYV!lE3uab+O9r=zla zUAfftY{%XR)@O~HHI0C~n*YnZzt*^qZFKw`LpP;#lFdN3fSrDS%fvqnsWu7^>qOUO z!8`X1gMjVEY*In}yU=|;ZEy4eiWZnn4~TOxK0#)E{0;QQ)YG3tA-)5$-BQc78lA-$ z%yRR|*jc^VrrY7&7IjR+Yz}B1W(Oe6Hkc~2nFhU}5?BWiBaI3^zrJ$arSm%;sOUov zH3J{mMR>WRml~mHZL#b&++*%A zx4K0Z1pOj@ZwDJM8}TI&${7o6&iyP7lQc>HcyymDsjKyF9GM>5^DA(xis8FB-iwYAqAb}|MG=kJ1jxhx}Zc#KWPx#Z> zZ{p;yehz>hRz18z_>c&PgseJ2u9q?tc!r0yf}kJL&x(<6bkkF+lD^vEIhG|VHvmVi zFf5uL7RxQM;4m!gqw=8(&AKJ)1X8dA?(43Yq)Gaxp!-}&Ez>ddmti+@7Ti(fP~ z#P$t$o(hc}zs<7pEn5VC23=|5FAl3mQ~JB(EOR|oju&%5I?Pa9z=i@*u}G<81f^sK zT2{$4b8sd+FWSuSQs<5+rib)V73>3og@)B~f0OFICje#cfd6iW3_ zf_)9=XsnUc&<-5Pw&_`g_?zHHOt-nT(O|c^hAoO3kX28d5E&QUYMgmY^EPK~Sj7eN z52KTH%uh$d`l$0iMEYMM%rPv<3wtR84r4;Gu$u78p!P{IZB2>2OHy>M(1RdKEk}*h z@RGMz=QlzQ%(2K}CxlN~b?B!mcBcRly(Bi}e(8cqnxuagx?hyk)~#ZCM?!IGhSigF zl9_!3c*MAO8WX`&R>pN0W0%1lpl!fL!3GyMn(OKi7`^D$kSQrGgZ+~VOvI zb)E7$<5|SbQ7Y^fLtl?bF0av1>85gA1zscU!8CnUIp3N~yQX72`zv2AZVyBkMI;xB zcpzpJ$4Oa0Vx4!DRg*MH|F-lGfgIG%tuS*gNm?OTfnY|he_IsyP4C6xE-Z8aCVAE5 zqu@gpie<>n8P9SWqfq1tRJoBgjM4!Nh8jH&6#n9b@VG$ej_UPQ2ok~4tgzTxNWTxd z*eLDnBIjWgO3 z&?ae;Ch70fKh%ivjlvFs%8E_I^LhwfJ$V`QdU~Gc5X%H5gIQ+WmKyU9*-W49<__jC z#1sxhmvBSI@6`30oEEHQiI zD0}Ilyr4?_EXST=8781cbF=MMR+%!tSOlI5b_m^Mgv&vH6=5VRNLeIh2U&9ZBu&yJ z-Ceq09Ork30!IK5#%k*&wF-r(LevdDuY`{3W7Pv49t_bUgRZ=m6Pd3_Wx*yRy%zMV z05^$vAjRmWe|7<$RHg7_2n(~yt|WCdXT8K{p0*kCN}0AEz$GfGVziscDTa=&l_KKarB!OoapKVri+U($uQi%mijZtvezr zd4@eNLl{YjcA3~&u>L(|rdJ^)BBetK#o=x`hx=^Lter;^(g#DBBccw^AOk=MPDsE9 z1UqANhrwnk*;25zgQFZF5Cu|>rouDm=VuhS)*gI+vuKhg>0grmKP&GlBIC(8k^lez M07*qoM6N<$f_T)fHUIzs literal 0 HcmV?d00001 diff --git a/website/src/assets/logo-light.png b/website/src/assets/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d488ef11091b77fc2749f5fd53716ba88cc92ea7 GIT binary patch literal 17032 zcmZ^K1x#H}^d|0Fw8be>+}%oxySr<#;{I@VcXxP?!UKxi!`Ch1qBKU3SC-COa%%Gn(ib19SPy%ErAzM{1K3wN~y?0L3zQS;!lTyB5=rVQ5O98fM6mkDF*fa-;>{7mh_Q==prpI zj(CJZip@z*yas0bNTPNT*K#p4b}{ESbu#}5px9a2xtZ8lnK)S0*m(H4*!kJ`7+6{P zSy_VtftCN4fStXWm4)a3U*ML4AL~Ql+y5!S#mdgy*~Qq-;s2H4VEf;N%)F`0K|zWB zk`@zH^H@I3@dW6pFAa1~G}oCau5gacb6d$4l$^9uA|aLgGg3yOR{Q(IP!`%!X89A9 zlegA>Ev%JUB3Kelg@H#|TPc=JrIF8cW#_nGKX~049g~}u&iwmJ{2I_C$Kia=caZBn z;hcML|1$0z{H{a?swBO-=NikA%hO1mS)WoxiiRByV9&?>2HhVnh5aj*-k2PQ*B=^? zFW~9|qyL>@674S(lZn}%rxxbh)-3Ht94Ch#6i%pD;R9Y3x@|_z?hED7!+w7T_kVvc zg^L`zO<4v1?7qc1v@wAL1(!nUmc%mo(;EpY!R^8n%(5jut3rC{i{P+emjk#UGDgT? zB{BSpBNW=*zcf1$#cT*Fr>G&6O}_(6pIXM&Vv5-5-_@_k3W|s6LCBIloQ#GFR&!KJ zB@4HMF~Kqb!$w45)DxIdIC}p;ish=FCe5)2g)m+gRl&+_Afhmg0ippcKG0e-lpUT> zUE?FOym(tTYIP0mHA|+7bJOS{b`F`qDz^v_m9dJhlO?_W0PmVx%kj1~e&r zZjX+h@4j@=u(g_m`HWc|T}AD%^)N`rY2F!&y33J_o`5fW&`Rp-B zzmpAR645%K6SfhW%rch~ST13%#%+*2rhGX2#{ztR;J(>Rn`^oHK%X6-@sA-5CJnk3 z#uIGmbWy`2h$le59`<$?Ijka1QGTo@xIV7} zSJr06HvD$F?&okcxz$qlja665b!O z5l+417TK#w?9EWWyHUe$dJ6ZMqDs-M`S`OK;{_V~P#2;ZDx(h^EfJ3V`e}})R@!xL zyeS~PQSEs#x3J{uG)KE0_uoLG)@mo?`qyB$YiqsGWjPJMc;};8V8_hrEu~*08Wh3C z$}u=39z5fWFo5_Rp0E704L_Rw!ZTu_tRjC4(pp#x3a{)69L)#e{}4EdkG&!Nzl zyR(*jCeFB@rT|x!FpNw7sqr(8`%mtE_(P7??~XUJUQ;)$e*v9&3{Fc3Y(H{el zXrXjbiGNV3SMZQ+bDPl4yjp`&=sPnp~`gG`TsK)LX_WIJt5-L;Uj-s9;y8m_V zSV1o}3AWC&B>75fj98!Mxct2mk-IWq0w{$rtn~Z9sA&e$;^prxwp%PVtv@@zB=4y{ zhCm>3qol*p?I(>jnnB=`J0$W*KRIYqI~7N0uBL4TRQz}-A4nt*b81F#9&!nH=I#f6krqNe@#;8-E~EJrQIdPEsu$1Gw(LU6 z8q+?6XpM}GaZq3g{aY#fHNfECkDDYUrT=EA7uDZGaog;0BxyxwBf$A=MW4%kOAKTV z0t+Ds=mDQxBco0Hm=^6Iijd7Zq_pne2%?iC`vtQOLyDhJ24`oj)sAsN2O=iuSAe@L zpl5Q`oE{CstqBHUqeyAEK-}^8mh;6Utu#3`b_XIJuitXwtUgv{-AE@%(wEIU0Etb2 z;HZB{vzB$YoRPhep^<>?c=~5ZC?!te6|4yZgx*yap}}o-dewOjL1eF`5UR zT?XX3Z?bt)<+j=X>UANXr9HUeDnrzd^PzVCjjh!xHrnEE>Zw0)cOkgYBNY&0Tku3|F8k7ojP%=L3wLhk~S%InC&QVHCdtb@Ux9CtU z&j-S9E1}iGy0q0S+H9~7$lOUingMAAor%jj1ea{geT2(&RB>W4h_tPP3Hc!idO_d9 zlX^kwCIcsZ^S3P5*n`}8i}DzV49>(qjYTo!ZI&Y>X4V9!LuQ^yyioK>m^f$0XtB>l z;kb9(G0I|%w|u@bI#a!B8gPebRVIyBajW@w;cSB9iH0!jEkJ%h?=L#rqij~4vZn^5YAtj}Cq4dxtR1;0C2>k+&D2EyBWnjDn+2+;> zCB((xHq`tL>=eZILP2r%S{5+ru=tgdA6+W1iqE!~jYN;M z?VL6ukcOIMdA|%R&t)v45ek@c48+RMtwfBSJV9R|uxhF=^d*r>q@--DQ;SJ(dm?x0{GAX z(`aS&AxEv|f8oPVC93Ym`x!is<|!!$Fr734zOzW9q8`wDc@WAIqeDv(0{nf}j$;4N zez)O~ekI+7)pvxLd|$V`ZdV^-Ni6Iiv0c}A8=Av1k%z80H#bxcYqyL9h~MziOD_0q z=O?qiY&0KYSMW-W(1Y&b&OSBq;G%4Rt@{#b{3DiwY7_CV!ap%6ZWHJAK8AMl2DOyF z$4F1&34gkb?+o#LX3VoX8pg=FfglRX2B{O4nK*V%8{wB&c7y}+F>^q9VX z9ZeKb+pJR0gUrO|qx|uIwkf^%zr{Gyt zS-!t09`5&VW=~S7YwgH@jTT26ot4L=*I!L}{iG<%P~Hd;^+c864v%q^#nqMJ zHxR!ho)ls{X5D}hzm>ym5^u6VrnKO>i1&Tjz_bo2K;cj5k_t#+If682mVQjG9%!4_ zGfhT$J|!k5W28VYLCbYa=#}0u4#B|noEA3$e%*3Lv?;X>v;?eRwBYXLr8STi z!PJkh096Hl1|8eo0s0w3Eq}Y>3mLmC_AY}kkJzNKAM&tVT>&vD zV~1=(d5&xSrD#vp3C6}Mb%vfQ(4esZUmpo$X zig2-nO2H;<)2op{mE5QN`RZ~Y8Y!=BdK4$K>q(@KztH!r^W#jeF%pK4Ijjjhl9_%` z7H~sMXRahVfVLJ>nw*vUXVi74mrqXniCJmRgi%7n?ch(f0t}65O%>F^Fv+CEMrw6V zZO?o26GXhWw7|zoTl`YR-_@B^4CszXKAboVAFO~Ngdmpyt@vG3{7+O`%a#jI@w$TI z#ry*!K-e{n9_~<(E_m$&6WxkgmH*3dD521el z_9ohC&BY&=hPBVn6Lh|sr_A*xNe@SYw9HVSh*Rn7M*LM_`G-*$t17hmL_R;i!6DN) zgEiJPK7JID@_KX5>)h`$f>}!6(|^BZuWa;H(MDd8`}4{3a80msB4?@Iz2Qtr)$7IYdf zK&*LgJm36-xncgkcE8(`0(#j}=KvrxPPqP&Z=3E&y#i(mso&Dr2J`)Eq`IJS=nQX5 z&t&vg?L07X=UB$E`EwQ#@*THzWlG+7ARDeN*$cXM%j*3)VlcF%I2Yi<9M}F9a89%4 zwyC~(U5t4330u3#$(%v=Tq{R!W%iQ$q8{2DRil}30O_=o|L1b1hE?cu0u!jqh<&zu z(?|fMH^0$l%*#Gq9{_WMVTn^=_FuJyXQo~jeF%P+cEd`OAfR(i^hi80b?Nc5bdBzh$o=1@& z-nUYsQ7m)YWLHWU#M5Rb#SWTsc765g(#}9@C||_Y6o+<;3C*iQV|L2?b}d0GQU|gN zzeRPtl*5faIznAiPSz|C|F0F-bsyGaa(tmzTIRMbV*_8yEw2Mx##k<`-J2vxpM=gG zI9YA_R>@UaovaC30QMY~Gh&cyPB)q9x0Wki2_AtB)ye9Xr!TSt65VFDOK8wXnUSdC z11XAuE}5xa7SUU_LR=2<3456ZZzOsQMCG(uoG>CxOrwwGTkWP>Jk_kB#2R3DWs{Y@ zO+7MUSRhQ5|F>c%mu29r2JNflA2>Vq$OdHJI{TRUEy1cvr=_5Y%cM@VuO@E25*L6c z+AnBlK1}%USy3crrU8c#g^CHtlMX|7N{r2Nmb6>f&bjJUl>OkY9cdnr+t9}X=1%}D zOF2gbNpE|F(*PUihP6E1Jn)#6U(eL3SU_zH6t0i|x9=OP2s5SP@~n3-=%DHoU< zSHB$ObmU*>d;5n)*r_|^{weIF1~2Tqo{8yHU+;Bs;?iO2yA-0RnM2l=ir+UpN2rw@rBV)O}gM%!fO;BS=A6)RDt?HI3+D0q! z8d#8`#0`&=EYPv)bsMYQNfM}p&tzs-7*+g4#3)err$$;EtT@B~=6|zS8Y%M28MfVq-%KDUyz!FrW6`Ncd+kn9;QQ)lT+xp^7XOOn_ z&z!n~^Wy-rv>4EfjjD}MtL-aCYCYh4OlP{d#Zg*PFpE<9wtrc<{Z6#h@(&d%SVDOo zDJg+KYluCk6r#T(2N6S!*e9$apn$3Vs_i{4B6AL3&2B2eh8K?kUk$>pS)T2agf$jq5Q#kQYph&0K!A4>3&EQor(d@U{DI$Y?}orm1I%gAcVjYMA6 z*4Nu#F@9+J{pWy)Y6CY8^`ufIwo4t9f#B=4rg0Y`KbwrpVM*V5^=W{e37ovW7`O8vKRXFi=Y zLH>hx>&UI*7+~A+9_Rm1n~UEca@|imw?Rq-ApFUn*b3F^DeY~sl6iC-8rRD`w_v`W zOon?mmq#q)tdF`E_7YP1#Q*mandbD(C$4L0hx7i1$=v&^>kb!@M*@4&kXOna$4GdpdKCw6HXf z42^~jI9YmX4E(R3(}8@y0sP*s zp6c7(4KH=y&Q}&{&Ue|8)io3c=xYin^%-h**Pg*F4y6D>$I-9ShMh$~0^=aUB?LRT z2aUeo%&>l+_ssbRK1;jFr$sI71i{KI?$Dc_N7+F~(#ZNhTY>gusLHS`EX04KQ;$1l zEcb$mgAP3HJst!x`ep&~U`JPo+L-CQR<=Vkm; z6#S-T%>qUlnQJXF0Zg@mGW)HJeHAop2H7lXBSPb#F#06w22B1eZwevfj?-^yZ_XE= zcd~3z9#uWz3z6+lC*2?saUdMqjmfM{pacZr*^N0s=b<*9n**(p*@lO@N7AwL&z|Zw8(R7z>gH5PRnARjQrkQ z1aDoEPS7&$muc~^5|(G2|6rN_W8M(^>GDbc+-7kNd&HKgsZwOgzs6S)?oBL`5 z*|M2(O>Dzfri_2Ok~1-!6wn@SHBwB^-g>x<-Vrh|7S_Q1%C6J(x-9-r;y zcz#7pAZvSy^VfIuMTdm=nSY0}GRm7GIXAbG6qvc=&-&1knAC3(7!x>Eb!zZ^Su}>! z6HC+7U^Ri(RJ6EHJr$B;SL1DG@_-FV3aV7trXByMg_2>p;$gb3X;GLA3_YLY<7*w} z*7GS%US~fPTD3r*#l{ah^8S8Q`rI^l* zg--_-fK(uhUhDxTM?aNObkKQ;rwgoQO}12e{i%Kp6K#rNdrQfw52L-M{mDtj<0SXZ zH2Qs8R~6CZRplPt1?}d&Y!?5gQFIQCjjzKkN~qLj54>fBoM_vatO(DQB`yV4dh|up z(0X)Sk(@uv=Y8vbCE??@=Eh-Pk_@y}0CTj~H5=481AQt$HOkAs8SfADu{}R~%w4~V zdp0XKZtwG0*($I;-sy+awmU*nVTVUg4qU@mK}WQx1Gtwd?RVnmt(biLIh5ZCHpWM* zv*mve#slxGAzfwzlf*_g6Zbv2ih(C*$E8>l@E{}(#5B~lgm7A%scd4wiPtn1b!8)E zpQ7n$DRK2NLc?39X%rd%IT&}!t8!o!gg&KPt+g|(51XX&+09W?uvzV{`Q-3moG;Rb ztP(cTbWh#%w~{$orIRo+-`y*$jd0=LQeuM*(6$1m=19K`?M>m?160|xw6Gcz$6SElz$LjPSnJ6&`Oo{l53m+r4H?)P|b7-U!5^f~HevqwMDXHnewsUtsMN zvQ*qGJN~}YQvRd-S!lPji2F)S%-$8#j@b6yP08EH(E8}}MDHhh`|b|j1y}+7tG>f{ zJ;}A71St5&U-I^zHnoTy&7@UdQO=q!jMfbr+VTSFL6xO1*Jabw>++tzSt_8Wp3wsA zOon=wR=X6t15vcLlQ@QtVeli7au0lA)X}O*rW~}O0CUx**Odt^8<_s~F?6Gy-|!XN z=zhRj`9DKf{%B76Yp#lrT|f7iX~1X8A)&1Loy6<#OvNDm8N&_z)JjYIMxc_i0{%$I zFs=T!HxJ&|-?bu0d{Y?Qh@DYohd%AKGSt_(HbA>oxpes5DGry4t(taFv{Qv)FOvmNu+&#j>TD43#E&aeMzA*cK3v z^HA-4c-{E^SN`V@Vi46CB_0-PG0)azdyu8m`(CSQG*y5Ac-{JY8X>B!(0i=2`3aBT zXETB!mwPymue95Gt7;o7tj<69wZFe!dKXr|hKI=FyM6h(U__9LIKhq>WrN*v(3kcB zf-PYjzuTqoAF_T|5HY&@%wRsJOI~IulMWzWobSuF$%ms#5M`Vkr9!BPgxOA_?@)g6qu-{}!Ps7k zW)$ra!xOvohU73XDl+9R-4G5mtSIg*=9FE}h;-gg)F!T;8+Q%qhyDZQYcKhNpnY$B zd-#{m4lq~6-qW1&vnu_wh$57f`NiGPU5^c!g;1Jr(5pHCif6g{0pY=_QSn4v9lLpz zLRt-rt^qE6?Y5_cT5nRz!YbMmKJB}sJ9|goxo^Fc;jl+Xme|1V#Y=S~(NjB)@n=cS zPri{3;Zomv_2XXa3`HkctU|PKK4`!X_ST1Co<0H!9>tK?D{Y8ny~%JfD^0hg^&Hpq zR@NCHGQz*mmW`#kdWSHnU|>w`=~>`5kZ=z+<@sfQSxL59!r{v&u)-k?bw4lcvi$|$ zkGv^&n1}3dr^`9KB4^Drwr&N%^qk3c)v3gC5yQt#Apz+Fmd)#O`U>;C9*TD}pi!iQ zOe&leV^PF)D|E4Y$*elo!znxaTIXId(NsEj{``ssdPY{3*5DZQwwF^{WwD{@M7ag^ zZml`js5jxqK@hi*#C@**#YCL%2d1dgNmcfjEjp7%O+X_$_! z#RY`n&aE7!Iv^=iQqfQ$=U}Q(rxVf%6>C2fLi3=YMUo-tV+CUR-0%kNlrsJ4xL1L5 z{7wXKA2Ic<>c;jHDZJZ!bw(RH$t6`viRVLyaNM9W(2Ljs4PP-5t;zP=F_i=Sg#HYP zAJ3d$0A9)|Nd}f;OF|J#bH{N)-g`LLmeX(gX0nRKIH2^*mEMF94w0a?Hk;#cgq{8+ zpXt?C(|_kbFc|A{T&(wPC;h?L=b-8joNv>!Ba2?-8!h6uwwp|W8%_7*nBu`L8SWC$ z<0GE}`{9`se_|Tcx*S!K=bq`+wYsa2EXUSk;-@?4xn#}Q%jnNU*8scz(rG%v$%%2<12Wq?ep5`QyhqfZ ze(u#I9$($_j$hVeX>NLmph3mEncj=;ackP26^b>gRpRRLYveV$uJK;NcddQ5P+^e?917VA0^TXg1Ot@*||Jrca zPY9HW)S6lT96vn&gMXHq(l{7!TR(_Rccng1?kaKxMqyCW$@rZw>E&bS4&+#==}`AR zYUR9`Y>6#$VZ+;%(3N_Fg!#yO^@K;Ki6N)97Z-U_Muy_eZ0GxZYO7{Q1o`aU#}uh; z0I^5)dX^i-37O``*B=ag(1R zQ9;kS!SIXr(p)a9-;#%OBNK~hUKt)09 zHLv-0LeoTP9x*@LQRt4cVICE%Ul#MhzW8>)E$m9kPg2tM-D4DLt6#-&(zSFFvs~7y zp=rx~-ulcu(Y$1G{e;L{U(w#)UTQNKw zjz?6lc_}c#ZB_Njnsb7ftFdEzq|gR1zh8|G>F?)Od`A)p^jAXQ=Vd;QO9#D<@6b%J zRe}eo-hWB7ug$**QsZJI$WbvB<~EFXFRse9f}BFJJ->J$WPPi&lD8=l7bWi%zkVbv2-0}}Kzk*yJ;HG!wyD6^0JqhciW~d>61P`8-{_BFqKBc6 z0mpP(sx9FhTbo(<6FyL$2EvOGfn}l?4^m+a?|feW1?M#G{}njjBSBLn?)$IJ3m z-@=(lZ(bKFKSvyW^?xC}>A8{rCW7eTu#&;=>!s{2=!lGOxr%i_AMRn1TH=4-E?{uU z@Atm=9e$1=ybjj9*K}G(#ZT}Z(a7rUz&M~zuQF<8pEtD5{zcZI>v|CvM|V2Uu%2{? zcE=%r8Zw+^=#$2x0f^Q2^ZaUQ=t$4=`Xu_nrq+$cBSf7{Ksws$7sF?>`Ujt5O;evb z8sckUqpZImpa>j7=ip$>WXb*Rt&Di=!c+J8M}CMamukdV z9vGdez~`v%_P+#(uQ< zb^BHB_u?0oGe7hqKq|aMb z*AwK^j~J4M_sid>^(N1ZK4DfyZ{8bBr@k4Xc(IPw<6PvtP`2zu*e5iY1lgp)R7Zk| z|EZsHP@;gDcbK&Rj7SoyTpdzxOHC4}&P74S?L7&V%FB~|ewieIDcLXjh_=G}d+4w8 zQGet}zNXIS8VLI>IDg!LuQ&B4RF%?EUvU8Dtp$RNN*!Dm=<57xu3Q?IOuaf*3}C8; z`ik*$=+&!jN_TY|BK-d4{PfwM;U<i0qL3n+ZXX~c7AvriE>^pt^>%D#3)yV4=7{kt{>Y=TPbivile-- zSE7>zu}z8obFRUy^KNz?#`3$R&vn+zS*~|^CTD$|O*)xI$L5CQ`Rv-Sb7`eyf+2~x zsb5qleKc96pRv{%HyGo^d=DB#vY;)$C|8&BuoMo!}24;!xBv-M5I zu!63K03OY3{)X`fq1e6Z${uzz-GGMvyI5C>0pw2`@SY_wGkjt?6nMqMj9sCs==cyH zyD?IZro*aM&oj-={Lim2{|Ml3E5m4E5#x)?Scpbd0m>fS1n>P+`!0R9_TOu4m*Jfr>82OJ2607XI(9iw-Qut z@6R@eCnx43nO7H?^HpootprPyHg>?}Erxf_?vdico*=;qA$`Mly$@}!6=?!kixMR{ z4%w$45p-vzmqvB)Hr2J9Ky2I?xn=3tj~f~x87;)@SV5*>bCau5r6e@$8Zn}gq@w46tQ0r6SfoF%8HbN|LyXCd~_tE2$4M)39_Gmb$ex;DNAFww-Tyuw^FSq22U6y$Fb~(Gz#xeOtcRdE$C6w>U$nwYt@?F z-0^6>UacR^HO8<+yQSeDSkPx0IS@f!uA4pm+mKtbZ_FIy!vQ7D9cSlW_lnKeE1d^$ z^0atEwUK7*li%5M^*kj?9Ht1$(>?symc!VPl~^ckayxcc_M;koH~kvhv0Daee@#(h zMAs)e-`SF=#*=lKBxjgyPF7~Jd%m@o@OSXq=bQ6<5>H7{VW*_#Qe)027+;e5j1oua zVPFx4cDLqW7HhhD^n^F!rJiE)3YF%$6&$RHWyS!F84b-T&O4c0I81IE`A`mRkL^Jq zS-=>POUmPuTaHsYT(uNx3ifoCUT2NLac2jfn^kfZ?~_zusW~3-P)2C zH5*v5XR+J>om+j_=rwz30J`fk!V{H%s_Dq#;K{R{h0&nytPw(8>=@w{s#S!*;$Kc4vtZJvTtyXIx#*|Vm`-Be}oW2!JXd)M-2zKCQY zew2L(A!F`1=mHJP$i8#h&mGNUYE<>ImD-~Lz`x+n$L`s<<2mk%8O~q0-2%%YM}3BU zdSkjv^Up+;S0w3*!vtOLN3nZXcZI3T;&$zR1N$Y2gw;Ti4+j4*ziB_jd1SyU!%RN# z$S7ip0Q&v_A76jOIBekIS?4<}=GT8KP80V7WUj9qOv@@0?wyy^I)`RDp72ilr{%)T zd+KE*vHI3C6DbBObD+M*giTL#-8Nz3A>d>v9`^^hL#p;T+HocJ_fV0FwAn-p1$cRq z{O0et$RNloF z1`V{wJj{_QbMbq;f4CLy5u|gG;*;#BX03R3+2xZBgu@NBq6ZM%xs_#QSCo4Eutv>L z?XkWl1ts9GTc2jlZvzO?h+wTYYsT*ZBcB%+1FEawaEBhEKL$SDX$;)c_lS&(S=tLm zJ|bEq$Hs==aeh?;oxa~{J#LZCm0s@Gjgjw}uWy7N4If#DL@PZXB<(d#$a#MOc`R7y zsfNUOiI8_!Pz>0{)&Fy@L22Q|b!xy(*r}L znHuU~MBZz#GZefS7=!AQEulqvwlxJ{2>QKt(lh{%cWo7x zf}5aPVjCeu@CfeaOX16E1p(PWa3khaQV4AMYqD*%0qq+QBK~x{11+GC zGSB6cx=Q;ccHPKW8&?qnT*77{!%2HW>~*&G@8kffIycWs9nZLq&=>!h9>9Fd7oo+X z8dWsiR7J$-$uPMK8!(Tq{e3A}cYlQMB81pFz6lc3??{@Zqu-Z-w^N?O8cW;)!gEgT ziardBC<7_54L!Sgu|+n1S&h0vVp4fQg^Yg6F4pJ9xhR91X$)1QdN6P#MWr?!5h~r@ z^IBvBA0}St)2L}g6?!gib{qM8mg3eF(4E~!$8PrUJl($QxnGGm7vW6H^JxhGIDmyr zoqN)Z)5PjIn*L$kn%M-wW)}}Xww{80a_hun3P1xxwA>EU!}!$0++FDEjZm@l;QUxn z72hDkVAMLs3s#mAb38=9!yu)s#)jw}Ll8}{061Zy>3pK-S9|gtn{du0?DK+J-}zRF zHqX6q?<(%yIPhfwWLOV~2+xj^$09Il<|=KSpsI>9Y1)?jO;;125SdfD23q6H= zkqqmhL4rYSzx;O+t9YL08#UIjVnfH7>Bj{o+}pDNd&uflJtkT;qj!(j6{Ks&$qSSK_jYPaVVA#Oqe4HBTirh<|_X-dDtL(#>vb}rW4~~z84y6xm z^K7=qOVCv?mA`{oB@tTR*>75@BV&UD@7xuS)dIoS zv7!q$<+QdL3c;Bc&)4~R__G`YkL2YN#+X0@eu<13RFRtmpP@wyy1oJ#5`00$1fiI2 znfS28Ej_a8F#{T+pCLocbZ^6CHM*>?=vmBP>o5_TZH2JD*xoodVDpuHV3Ly0=e3BK z?>i*EUMB^Bi=HzP^>*~aRgX?%t5GqN<|!k2_i{AP$YL*|Mi=YWqxteo*2M=`de&7{;4L_Z831>7Jg)U}B)cf>#r~>F>2B<) zu~BKL{w#Q2skCeoq~mr3>X=XJfuG>|_m};bp8j(oHXppe@0aM7vf?8_Vb_zmoV`Hr z*pee5Sgjc~LrLeKMWMvbsN5^TlFKMrA!GRtcy5QY?lpgdh;6INHvuaW>hDYUXgG9&JX(+xzYO_Y9FyT)nWUCS9PqM>>}K0|?!)>c?MuJm0v?a9d>(Ok zRk1Ld&0Q=7Ndf~*;a-WSU_;ZA;X_POxnX^qsF?4M<>0kR#e@C9(w7_c*;-GDdc7|TAb`){jMn{9s-$ z?S|<*<4(NohF$C?y}VTWg&q@N#lqAu6Aw@=xZVjGMFY^YRxjWyTmKd;x&Ayegmy5o zr{wz_KcVo2=%O+(C-4(&tXb+tA`!31gV|j-mt6a%;}?xNcJpDEPr1)tgXueByq{Yk z!cgyFNB3NdefJyv7*BD&9*fZN7dIWvHbISa;G?7p#Rbg`zsvT$r?SRlt*eLTUp*Ng zI-3tXQNruzyVn>@Wp_;OqOLvTO>v@B#7l( z!x!F?3g0oD*0Idt=f=FyvpYP4g)6QK{Pjm3~Zkt<0FKMGzBgWm~zU>+Ks_nm$lx-`Q;7qbtW;*2Efjzt4hMXx|4x z3(iq$=&7BN1>-DhRi@zHD5rP93YDrjP&!uH)h~W^fdh46@TeZr$t1M0`17UdS@uM-Y?`n7tkL7P&ho-NFW?$XNoT!IRol{WH4t14?ZI2sp4w=@481XDK zkj~4mxNj8WMc$8IhoVBm@d;Gl#yYoJgtx#MnL5T`SZ;nB@N6!;)`*>fs*kFZiEH2d zP;n=dRqoDe|6Cjk&Qj=yG2S;f$%Ab*vVL&z;#t=}h0o{v_!TWdZ-y{{CQ)kFfoeZF z!G!z*A<5qz_Z>4eTQd-VK05JCQ_Ed_Z=Rxihs$*nxO3;G_3iG#`OSdG0J5s82%$De zq`LR&ddx1iOz}<6V45u-`+ZOibv_MnACs+_3^oNkAWP+Hr8c8v*gyjU*CM$>q#u8w z?b?Q|mn9ic@Uq1#hC(vr0|^S7STPrF|HYQjZpCV`?Dh2(G#!V2RUQZrzq{#OQ@vlK zahyTDJ{gO`LgACEuG-w8X5eX}qRMhoQ@0q+W*kZ57Q>O|L9{Vf#46UB+FYZTZ%0Hl zN;5%rbMsLWW471R7a+scujP>Q_P+x9@HE>e=E@G5ZMafj3s=9dr!Co;u*v@!9`f@k zI;Xz&$qQUvRZ}bGmKFaD_~w)P6#HSjkd4JUW7>9t7cV41}>!=#Y7KPTv6DFmTUk|WV@PRQ2*hqQkRRVd`a&uy~Vi+E< zkZWfvdbotNl-B<_TVyl+Wn*31R$D2?gyl(vZ(D8luWckq885V@(G?;N-#nF-%B;Uj z;_`m(v2PQWq-gm!>g5{VIkDz5_ku2KMR>8B3-Y7XpH0KIKN5zoQeJU47k;++ykl7? zjOF=4sdnTQ?1E~z?-*gZ3_fGBl+G7><>*3*igN6R7z_79abg#Ef*Z6S#)mk_X{!$5 zTI@pe+~zDi>f*#Mq4ZGeTawLmHq8Ok!#5miEHp~A#3dgEcI+i|Y z^X`!{q*_+z^q43*@jN3WVo^O|*CqV?nyO{*7iT{LlriXdqQ~es5zO`2S6}zKw*kE0 zh%@T9#3~KiDtZ1EGkEk}_^l+3-sbVcsz!TeRTu$OA;3Xo{9znd${IKA7_t>RuGL)q z0A8G>%)ACsrDq8`C_7o^274*9+DX+U@E@?!_CyYgrJv6 zkec<$IuTLOk!xa%KiTUn70Ww&bLXWO3kCV1E7zmq)-zE>5dXH{kL-oP!g>lZ%6zR+ zZxmD1sbm8E?5pb?bwiSxb|5xcblAHhnJ*(4;mbF3%z3~M)FKfk_KVj{Wwun>asJ>U zdqCv9X2U00s_@_+vDG@HC}BivV%@0GGk*8h5z0$~?3LKqZBqS6Q@_%+Y<=DoRDDQR z>|2>EeP2=a0N1vkq}_Qzxhzn=fnyK{dC5w5&qYTGe36Xc z`hFCtGLz3TM**k<`=RfgenTb%M)a6<+3gXNZ`7E+TjV=%IPnE9{`#@?zQpw&U9t$y zjtXt{4Lq6d(Q(Jnr*JI9oACLGDL=Tq16CW9T*_{{iB!Ef?v64b9utvwO){6b+4tn#c+vqKO$E zHeamp0Gt@3+#U?;JxaUke zTMENZ!L3&a6EjGuJ%PcTj=CJ++T<+SNJ6p}yk?cxs%Nn)b!vi(@57%{KI(J3mo!nz zoh?HWff$s|AMhI&CHGZ`;C+WXKNcqSlxUz?CgBh!6Q)m`t&kA+(q|UYFLyI9>)?J> zMaty|%sV^Tqt~#YjJ4r6jb}6#*;!opqR2}?Yx56*Bg12=(IAN{wGwQk82a*$>QR{0 z75sJ2b)-2Exj_Q92;e6zXLeb^+0S5M^nR0h`+rdA?oUi|#=?#z> zTJ8U{o9-iSG(q;uwB&+8HRJ5JG(2?;G>8P#g(RiQ;l!a`P|AV zbHIoxq&bz-bxGCtr3{mL!S|IeeRlq6`(YAh|BJMXP+BG2)^x@0! zuj$()#f=T@NW$T9vvhhmg}0s?WT0HSUMi99IKXdbm-qiXDP_1vxR0Uz_Wocp$!g0m z7>poGc0dCVTV?#dye#F+sv0GJsABTAs|w;`Tpw^)Z;9Y23r*6AT4P zIJ&*mfY>mAwh|jYnoI&iOfjc=93%HfF)m*l{}6UIHYVLVfU zvgI6p1E-5BSrdFu+|4X$$h0y%7kv@C+)`z6HW{hoCruF-)RpnYfcSxVTPE-BEf5A9k^Zo~F5YmrDX1pEZZ|^-LYk+#SwUf1Fa7 zJUn>F^{1?&-;I6rP`TD)oDs&I=_yQm)xaHB%J>!6pOB6x&!OTp*l@N)D3e&z7L7Tk zV-hL2eP>wUyVtd9yZt>y54`+oZL>0+!KijsXQWb)({ z7hd0DUaw(hhvo$51|~CB_|C{Bmex!OoWgmEO0R8W{TB3-6lr!7r%yI5Hy4AFfk^7dFL?r%>|4OIu=3L`E8&nxY*e%{4iZJa6r= z5GjwXcZfQIVw|)8+SiKKQy{=OKmYkbs=|Jz_+dKjRmBTeAMWgb0<-~3{uAIm8D+xC z7@ZW*J2^qLWVT+9LAQ)pInu3(-9eOoa-LJ}c@M>EJyo4e~q zWmG=ZvRQ!hjS<6EfP=793h^XP*oouhgI^UzXBM$!2Oo9?XA6P6R4ykhR;0ZOs|4>& z&}R(HRYL6^LO$yzgyG8x!Uuz?G1g#n12$){dAgz|m7Gr1;$OAE>w^1|LujcBt^hOn zI}7+a6)a|ze0Fnp+o+7nr(QlamQ+-i633S!+NVY-*tM6?tOT;950gPl1VSl5JP8sL zWUZpB6j>nn?}_TXd|BJnMwy>WJ4td%9n5}8=nzGwry_fr0>Jz7atoROZ3C?bsTFjA z`1-LCuQnzn-LQ@@kYp-jSVcDzNJ?ZA?{-v1WmE$B6e+3I#Hl2YdC`bQ$0S6?yHE~V(J7R35poH<*nm9+Yjz + + +
    + + +
    + + + diff --git a/website/src/components/PageFrame.astro b/website/src/components/PageFrame.astro new file mode 100644 index 0000000..ceb5393 --- /dev/null +++ b/website/src/components/PageFrame.astro @@ -0,0 +1,135 @@ +--- +import MobileMenuToggle from '@astrojs/starlight/components/MobileMenuToggle.astro'; + +const { hasSidebar } = Astro.locals.starlightRoute; +--- + +
    + +
    + { + hasSidebar && ( + + ) + } +
    +
    +
    + + diff --git a/website/src/components/PageTitle.astro b/website/src/components/PageTitle.astro new file mode 100644 index 0000000..7d5f9c4 --- /dev/null +++ b/website/src/components/PageTitle.astro @@ -0,0 +1,54 @@ +--- +const { entry, sidebar } = Astro.locals.starlightRoute; + +type Entry = (typeof sidebar)[number]; + +function currentGroup(entries: Entry[], parent?: string): string | undefined { + for (const item of entries) { + if (item.type === 'link' && item.isCurrent) return parent; + if (item.type === 'group') { + const found = currentGroup(item.entries, item.label); + if (found) return found; + } + } + return undefined; +} + +const group = currentGroup(sidebar); +const { title, description } = entry.data; +--- + +
    + {group &&

    {group}

    } +

    {title}

    + {description &&

    {description}

    } +
    + + diff --git a/website/src/components/Pagination.astro b/website/src/components/Pagination.astro new file mode 100644 index 0000000..ddf633b --- /dev/null +++ b/website/src/components/Pagination.astro @@ -0,0 +1,61 @@ +--- +const { prev, next } = Astro.locals.starlightRoute.pagination; +--- + +{ + (prev || next) && ( + + ) +} + + diff --git a/website/src/components/SiteTitle.astro b/website/src/components/SiteTitle.astro new file mode 100644 index 0000000..35fc74b --- /dev/null +++ b/website/src/components/SiteTitle.astro @@ -0,0 +1,58 @@ +--- +import logoDark from '../assets/logo-dark.png'; +import logoLight from '../assets/logo-light.png'; +--- + + + + diff --git a/website/src/components/ThemeSelect.astro b/website/src/components/ThemeSelect.astro new file mode 100644 index 0000000..9618911 --- /dev/null +++ b/website/src/components/ThemeSelect.astro @@ -0,0 +1,53 @@ + + + + + + + diff --git a/website/src/components/TwoColumnContent.astro b/website/src/components/TwoColumnContent.astro new file mode 100644 index 0000000..c1d70e1 --- /dev/null +++ b/website/src/components/TwoColumnContent.astro @@ -0,0 +1,43 @@ +--- +const { toc } = Astro.locals.starlightRoute; +--- + +
    +
    + { + toc && ( + + ) + } +
    + + diff --git a/website/src/components/docs/Card.astro b/website/src/components/docs/Card.astro new file mode 100644 index 0000000..3145537 --- /dev/null +++ b/website/src/components/docs/Card.astro @@ -0,0 +1,65 @@ +--- +import ArrowRight from '@lucide/astro/icons/arrow-right'; +import BookOpen from '@lucide/astro/icons/book-open'; +import Code from '@lucide/astro/icons/code'; +import Compass from '@lucide/astro/icons/compass'; +import Eye from '@lucide/astro/icons/eye'; +import FlaskConical from '@lucide/astro/icons/flask-conical'; +import FolderTree from '@lucide/astro/icons/folder-tree'; +import Gauge from '@lucide/astro/icons/gauge'; +import GitBranch from '@lucide/astro/icons/git-branch'; +import Hash from '@lucide/astro/icons/hash'; +import Layers from '@lucide/astro/icons/layers'; +import List from '@lucide/astro/icons/list'; +import Lock from '@lucide/astro/icons/lock'; +import MapPin from '@lucide/astro/icons/map-pin'; +import Network from '@lucide/astro/icons/network'; +import Rocket from '@lucide/astro/icons/rocket'; +import ShieldCheck from '@lucide/astro/icons/shield-check'; +import SlidersHorizontal from '@lucide/astro/icons/sliders-horizontal'; +import Terminal from '@lucide/astro/icons/terminal'; +import TestTube from '@lucide/astro/icons/test-tube'; +import TriangleAlert from '@lucide/astro/icons/triangle-alert'; + +interface Props { + title: string; + href?: string; + icon?: string; +} + +// Font Awesome card icon names mapped to Lucide icons. +const icons = { + 'arrow-right': ArrowRight, + book: BookOpen, + code: Code, + 'code-branch': GitBranch, + compass: Compass, + 'diagram-project': Network, + eye: Eye, + flask: FlaskConical, + 'flask-vial': TestTube, + 'folder-tree': FolderTree, + 'gauge-high': Gauge, + hashtag: Hash, + 'layer-group': Layers, + list: List, + 'location-dot': MapPin, + lock: Lock, + rocket: Rocket, + 'shield-check': ShieldCheck, + sliders: SlidersHorizontal, + terminal: Terminal, + 'triangle-exclamation': TriangleAlert, +} as const; + +const { title, href, icon } = Astro.props; +const Icon = icon ? icons[icon as keyof typeof icons] : undefined; +if (icon && !Icon) throw new Error(`Unknown card icon: ${icon}`); +const Tag = href ? 'a' : 'div'; +--- + + + {Icon && diff --git a/website/src/components/docs/CardGroup.astro b/website/src/components/docs/CardGroup.astro new file mode 100644 index 0000000..8db4c82 --- /dev/null +++ b/website/src/components/docs/CardGroup.astro @@ -0,0 +1,8 @@ +--- +interface Props { + cols?: number; +} +const { cols = 2 } = Astro.props; +--- + +
    diff --git a/website/src/components/docs/Frame.astro b/website/src/components/docs/Frame.astro new file mode 100644 index 0000000..796a178 --- /dev/null +++ b/website/src/components/docs/Frame.astro @@ -0,0 +1 @@ +
    diff --git a/website/src/components/docs/Note.astro b/website/src/components/docs/Note.astro new file mode 100644 index 0000000..d721ca8 --- /dev/null +++ b/website/src/components/docs/Note.astro @@ -0,0 +1,8 @@ +--- +import Icon from '@lucide/astro/icons/info'; +--- + + diff --git a/website/src/components/docs/Warning.astro b/website/src/components/docs/Warning.astro new file mode 100644 index 0000000..8a06507 --- /dev/null +++ b/website/src/components/docs/Warning.astro @@ -0,0 +1,8 @@ +--- +import Icon from '@lucide/astro/icons/triangle-alert'; +--- + + diff --git a/website/src/content.config.ts b/website/src/content.config.ts new file mode 100644 index 0000000..d9ee8c9 --- /dev/null +++ b/website/src/content.config.ts @@ -0,0 +1,7 @@ +import { defineCollection } from 'astro:content'; +import { docsLoader } from '@astrojs/starlight/loaders'; +import { docsSchema } from '@astrojs/starlight/schema'; + +export const collections = { + docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), +}; diff --git a/website/src/content/docs/docs/benchmarks.mdx b/website/src/content/docs/docs/benchmarks.mdx new file mode 100644 index 0000000..c3b8427 --- /dev/null +++ b/website/src/content/docs/docs/benchmarks.mdx @@ -0,0 +1,64 @@ +--- +title: "Benchmarks" +description: "SQLClean, sqruff and SQLFluff linting the same 10,282 Snowflake SQL files." +--- + +SQLClean 0.0.1, sqruff 0.44.0 and SQLFluff 4.4.0 linting the same [10,282 SQL files](#the-corpus) on +a Linux machine with 4 CPUs. Each tool runs its default rules on the Snowflake dialect, writes JSON +findings, and starts with no cache. SQLClean and sqruff cells are the median of three rounds; +SQLFluff ran once. + +| Command | Wall time | CPU time | Peak memory | vs SQLClean defaults | +|---|---:|---:|---:|---:| +| `sqlclean check` (13 default Rules) | **0.81 s** | 2.5 s | 48 MiB | 1× | +| `sqlclean check --select SQC` (all 45 Rules) | **1.33 s** | 3.8 s | 140 MiB | 1.6× | +| `sqlclean format --check` | **3.03 s** | 11.5 s | 49 MiB | 3.7× | +| `sqruff lint` | 41.0 s | 114 s | 235 MiB | 51× | +| `sqlfluff lint --processes 4` | 919 s | 2,796 s | 992 MiB | 1,135× | + +Run again with nothing changed, `sqlclean check` reads its result cache and finishes in 0.23 s. +sqruff and SQLFluff have no result cache, so a second run costs the same as the first. + +## Comparing like with like + +The tools don't check the same things. Almost every sqruff and SQLFluff finding on this corpus +comes from a layout rule, such as indentation (`LT02`) or line length (`LT05`). SQLClean leaves +layout to its [formatter](/docs/formatting/), so the fair comparison is `sqlclean check` plus +`sqlclean format --check`: 3.8 s together, against 41 s for sqruff and 919 s for SQLFluff. + +The finding counts show that each tool read every file, not how many problems each one finds: + +| Tool | Findings | Parse errors | +|---|---:|---:| +| SQLClean, default Rules | 3,872 | 0 | +| SQLClean, all Rules | 34,322 | 0 | +| sqruff | 258,378 | 0 | +| SQLFluff | 266,367 | 0 | + +## The corpus + +The files come from DuckShop 10K, the generated project SQLBuild benchmarks its compiler with. It +describes a fictional home goods business: storefronts, marketplaces, carriers, ledgers, ad networks +and support tools, with every name invented. Compiling the project expands its macros, and +`benchmarks/linters/convert_duckshop.py` then replaces SQLBuild's reference functions with plain +table names, so every linter reads ordinary Snowflake SQL. + +| What | Count | +|---|---:| +| SQL files | 10,282 | +| Staging, intermediate and mart models | 6,149, 1,961 and 2,162 | +| Size | 44 MB | + +## Run it yourself + +The harness lives in `benchmarks/linters/`. Its README has the steps to build the corpus; +then: + +```bash +pip install sqruff sqlfluff +benchmarks/linters/bench.sh /tmp/corpus/sql /tmp/linter-bench 3 +python benchmarks/linters/summarize.py /tmp/linter-bench +``` + +Each round copies the corpus into a fresh directory and runs every tool in alternating order. +`/usr/bin/time` records wall time, CPU time and peak memory. diff --git a/website/src/content/docs/docs/cli/check.mdx b/website/src/content/docs/docs/cli/check.mdx new file mode 100644 index 0000000..bbfc420 --- /dev/null +++ b/website/src/content/docs/docs/cli/check.mdx @@ -0,0 +1,158 @@ +--- +title: "sqlclean check" +description: "Report Rule findings without changing any file." +sidebar: + label: "check" +--- + +`sqlclean check` runs the selected Rules over SQL files and reports every finding. It never changes +a file. + +## Usage + +```bash +sqlclean check [OPTIONS] [PATHS]... +``` + +```bash +sqlclean check # every matching file under the configuration root +sqlclean check models/orders.sql reports/ # only these files and directories +sqlclean check --select SQC001,SQC004 # only these Rules +sqlclean check --output-format sarif > sqlclean.sarif +``` + +With no paths, `check` reads every file under the configuration root that matches `include` and +not `exclude`. A directory is walked the same way. A file named on the command line is read even +when `include` does not match it, unless `exclude` does. See +[Choosing files](/docs/configuration/#choosing-files). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to read; the configuration root when omitted | +| `--select ` | Rule codes or prefixes to run instead of `[rules] select` | +| `--ignore ` | Rule codes or prefixes to skip, in addition to `[rules] ignore` | +| `--no-cache` | Evaluate every file instead of reusing results for unchanged inputs | +| `--output-format ` | `text` (default), `json` or `sarif` | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +`--select` and `--ignore` take a comma-separated list, or the flag repeated. A value is an exact +code such as `SQC001` or a prefix such as `SQC` or `XSQCNAME`. A value that matches no Rule is an +error. See [Selecting Rules](/docs/rules/overview/#selecting-rules). + +## Text output + +```text +error[SQC004]: Row selection is nondeterministic + --> models/customers.sql:6:1 + | + 6 | LIMIT 10; + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +error[SQC001]: Comparison with NULL is never true + --> models/orders.sql:8:22 + | + 8 | WHERE o.cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error: stale rule exception: SQC004 in models/orders.sql matches no finding; remove it from sqlclean.toml + +Found 3 problems (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +Findings are sorted by path, then position. Each one shows the Rule code and message, the +location as `path:line:column`, the source line, the remediation, and the command that applies its +fix when it has one. [Stale exceptions](/docs/rules/overview/#stale-exceptions) follow the +findings. A clean run prints `All checks passed.` + +Paths are relative to the configuration root, with `/` separators. Lines and columns start at 1 +and count characters, not bytes. + +## JSON output + +`--output-format json` prints one document: + +```json +{ + "findings": [ + { + "code": "SQC004", + "path": "models/customers.sql", + "line": 6, + "column": 1, + "end_line": 6, + "end_column": 6, + "message": "Row selection is nondeterministic", + "remediation": "Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET.", + "fix": null + }, + { + "code": "SQC001", + "path": "models/orders.sql", + "line": 8, + "column": 22, + "end_line": 8, + "end_column": 23, + "message": "Comparison with NULL is never true", + "remediation": "Use IS NULL or IS NOT NULL when testing for NULL.", + "fix": "unsafe" + } + ], + "stale_exceptions": [ + { + "rule": "SQC004", + "path": "models/orders.sql", + "relation": null + } + ], + "summary": { + "findings": 2, + "fixable": 1, + "stale_exceptions": 1 + } +} +``` + +`fix` is `"safe"`, `"unsafe"` or `null` when the finding has no fix. `summary.fixable` counts +findings with a fix of either kind. + +## SARIF output + +`--output-format sarif` prints a SARIF 2.1.0 log for code-scanning tools. Every finding is a result +with `level` `error`, its Rule code as `ruleId`, and its message and remediation as the result +text: + +```json +{ + "ruleId": "SQC001", + "level": "error", + "message": { + "text": "Comparison with NULL is never true. Use IS NULL or IS NOT NULL when testing for NULL." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { "uri": "models/orders.sql" }, + "region": { "startLine": 8, "startColumn": 22, "endLine": 8, "endColumn": 23 } + } + } + ] +} +``` + +The SARIF log lists findings only. Stale exceptions still make the command exit `1`; read them +from the text or JSON output. See [CI](/docs/integrations/#ci) for an upload example. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | No findings and no stale exceptions | +| `1` | At least one finding or stale exception | +| `2` | The command could not run: invalid arguments, configuration or input, or a custom Rule that failed | + +Errors are printed on stderr as `sqlclean: error: `. diff --git a/website/src/content/docs/docs/cli/facts.mdx b/website/src/content/docs/docs/cli/facts.mdx new file mode 100644 index 0000000..8e29b53 --- /dev/null +++ b/website/src/content/docs/docs/cli/facts.mdx @@ -0,0 +1,77 @@ +--- +title: "sqlclean facts" +description: "Print the statement facts custom Rules read for SQL on standard input." +sidebar: + label: "facts" +--- + +`sqlclean facts` parses SQL from standard input and prints the structural facts of each statement +as one line of JSON. These are the facts a [custom Rule](/docs/custom-rules/overview/) receives, +so use it to see what a Rule will read before writing one. + +## Usage + +```bash +sqlclean facts [--dialect ] < query.sql +``` + +```bash +echo "SELECT o.order_id, COUNT(*) AS items FROM orders AS o JOIN order_items AS i ON i.order_id = o.order_id WHERE o.status = 'paid' GROUP BY o.order_id;" \ + | sqlclean facts --dialect duckdb +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--dialect ` | The dialect to parse with (default: `generic`); see [Dialects](/docs/configuration/#dialects) | + +`facts` reads no configuration file and no paths. + +## Output + +The output, pretty-printed here and trimmed to some of the columns: + +```json +{ + "statements": [ + { + "index": 0, + "kind": "select", + "start": 0, + "end": 146, + "terminated_end": 147, + "ctes": [], + "tables": [ + { "start": 42, "end": 48, "name": "orders", "qualified_name": "orders", "alias": "o" }, + { "start": 59, "end": 70, "name": "order_items", "qualified_name": "order_items", "alias": "i" } + ], + "joins": [ + { "start": 54, "end": 102, "kind": "INNER", "relation": "order_items", "has_condition": true } + ], + "stars": [], + "functions": [ + { "start": 19, "end": 24, "name": "COUNT" } + ], + "columns": [ + { "start": 7, "end": 17, "name": "order_id", "qualifier": "o" }, + { "start": 109, "end": 117, "name": "status", "qualifier": "o" } + ], + "literals": [ + { "start": 120, "end": 126, "kind": "string", "text": "'paid'" } + ] + } + ] +} +``` + +Offsets count characters from the start of the input, not bytes. Custom Rules receive the same +facts with `line`, `column` and `text` added to every node. See +[Statement facts](/docs/custom-rules/statement-facts/) for every field. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | The facts were printed | +| `2` | The SQL does not parse under the dialect, or the dialect is unknown | diff --git a/website/src/content/docs/docs/cli/fix.mdx b/website/src/content/docs/docs/cli/fix.mdx new file mode 100644 index 0000000..d054888 --- /dev/null +++ b/website/src/content/docs/docs/cli/fix.mdx @@ -0,0 +1,98 @@ +--- +title: "sqlclean fix" +description: "Apply Rule fixes until the files stop changing, then format them." +sidebar: + label: "fix" +--- + +`sqlclean fix` applies the fixes of the selected Rules, checks again, and repeats until no fix +applies. It then formats every file it read and reports the findings that remain. + +## Usage + +```bash +sqlclean fix [OPTIONS] [PATHS]... +``` + +```bash +sqlclean fix # apply safe fixes, then format +sqlclean fix --diff # print the changes without writing them +sqlclean fix --unsafe-fixes # also apply fixes that can change query results +sqlclean fix models/orders.sql # only this file +``` + +Files are chosen as for [`sqlclean check`](/docs/cli/check/). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to read; the configuration root when omitted | +| `--unsafe-fixes` | Also apply fixes their Rules mark unsafe because they can change results | +| `--diff` | Print the changes as a unified diff instead of writing them | +| `--select ` | Rule codes or prefixes to run instead of `[rules] select` | +| `--ignore ` | Rule codes or prefixes to skip, in addition to `[rules] ignore` | +| `--no-cache` | Evaluate every file instead of reusing results for unchanged inputs | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +Only fixes of selected Rules apply. `sqlclean fix --select SQC005` removes unused CTEs and +formats the files, and leaves every other finding alone. + +## Output + +```text +Fixed 1 problem(s) and changed 1 file(s). +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:7:24 + | + 7 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +Found 1 problem (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +The summary line goes to stderr. The remaining findings go to stdout in the same format as +[`sqlclean check`](/docs/cli/check/#text-output), with positions in the fixed file. + +With `--diff`, nothing is written. `fix` prints a unified diff of every file it would change, +followed by `Would fix N problem(s) and change N file(s).`, and does not list the remaining +findings: + +```diff +--- a/models/orders.sql ++++ b/models/orders.sql +@@ -5,7 +5,7 @@ + FROM orders o + JOIN customers c + ON o.customer_id = c.customer_id +-WHERE o.cancelled_at = NULL ++WHERE o.cancelled_at IS NULL + GROUP BY + o.order_id, + c.customer_name +``` + +## How fixes are applied + +- Each pass checks the files, then applies every eligible fix whose edits do not overlap an earlier + fix in the same file. A skipped fix is reconsidered on the next pass. +- A pass that would turn a file that parses into one that does not is not applied to that file. +- `fix` stops when a pass applies nothing, and after at most 10 passes. +- Safe fixes always apply. Unsafe fixes apply only with `--unsafe-fixes`. See + [Safe and unsafe fixes](/docs/rules/overview/#safe-and-unsafe-fixes). +- Fixes from [custom Rules](/docs/custom-rules/fixes-and-options/) apply the same way. +- Every file is formatted after the last pass, whether or not a fix changed it. A file the + formatter cannot format is reported as `: could not format: ` and keeps its fixes. + +A file that does not parse gets no fixes and is reported as +[`SQC999`](/docs/rules/overview/#parse-errors-sqc999). + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Every file is fixed and formatted, and no finding or stale exception remains | +| `1` | Findings or stale exceptions remain, a file could not be formatted, or `--diff` found changes to make | +| `2` | The command could not run: invalid arguments, configuration or input, or a custom Rule that failed | diff --git a/website/src/content/docs/docs/cli/format.mdx b/website/src/content/docs/docs/cli/format.mdx new file mode 100644 index 0000000..8582313 --- /dev/null +++ b/website/src/content/docs/docs/cli/format.mdx @@ -0,0 +1,76 @@ +--- +title: "sqlclean format" +description: "Rewrite SQL files in the canonical layout, or check that they already are." +sidebar: + label: "format" +--- + +`sqlclean format` rewrites SQL files in SQLClean's canonical layout. It runs no Rules and applies +no fixes; use [`sqlclean fix`](/docs/cli/fix/) for both. + +## Usage + +```bash +sqlclean format [OPTIONS] [PATHS]... +``` + +```bash +sqlclean format # rewrite every matching file +sqlclean format --check # fail when a file needs formatting +sqlclean format --diff reports/ # print the changes for one directory +``` + +Files are chosen as for [`sqlclean check`](/docs/cli/check/). Each file is formatted with its own +[dialect](/docs/configuration/#dialects) and the configured +[`line_width`](/docs/formatting/#line-width) and [`indent`](/docs/formatting/#indent). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to format; the configuration root when omitted | +| `--check` | Fail when a file needs formatting, without writing it | +| `--diff` | Print the changes as a unified diff instead of writing them | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +## Output + +```bash +sqlclean format --check +``` + +```text +j.sql: could not format: Parse error at line 1, column 16: Expected Colon, got RBrace ('}') near [{{ col }} from orders] +Would reformat: models_a.sql +1 file(s) would be reformatted, 2 file(s) already formatted. +``` + +`--check` names each file that would change. `--diff` prints a unified diff instead: + +```diff +--- a/models_a.sql ++++ b/models_a.sql +@@ -1 +1,5 @@ +-select order_id, amount from orders where amount > 0 ++SELECT ++ order_id, ++ amount ++FROM orders ++WHERE amount > 0 +``` + +Without either flag, files are rewritten in place and the summary reads +`N file(s) reformatted, N file(s) already formatted.` The summary and the `could not format` +lines go to stderr; the `Would reformat` lines and diffs go to stdout. + +A file the formatter cannot format, usually because it does not parse under its dialect, is left +unchanged and reported with the reason. See +[When a file can't be formatted](/docs/formatting/#when-a-file-cant-be-formatted). + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Every file is formatted, or was rewritten | +| `1` | A file could not be formatted, or `--check` or `--diff` found a file to change | +| `2` | The command could not run: invalid arguments, configuration or input | diff --git a/website/src/content/docs/docs/cli/rules.mdx b/website/src/content/docs/docs/cli/rules.mdx new file mode 100644 index 0000000..a9fd64b --- /dev/null +++ b/website/src/content/docs/docs/cli/rules.mdx @@ -0,0 +1,112 @@ +--- +title: "sqlclean rules" +description: "List the built-in and custom Rules, or show one Rule's details." +sidebar: + label: "rules" +--- + +`sqlclean rules` lists every Rule the configuration can select: the built-in Rules, then the custom +Rules under `[rules] custom`. With a code, it shows that Rule. + +## Usage + +```bash +sqlclean rules [OPTIONS] [CODE] +``` + +```bash +sqlclean rules # list every Rule +sqlclean rules SQC001 # show one Rule +sqlclean rules --json # every Rule as JSON +sqlclean rules --json SQC001 # one Rule as JSON +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `CODE` | Show this Rule's details; an exact code, not a prefix | +| `--json` | Print Rules as a JSON array | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +## Listing Rules + +```text +SQC001 default Comparison with NULL is never true +SQC002 default Comma-separated sources create an implicit cartesian join +SQC003 default Non-cross join has no meaningful condition +SQC004 default Row selection is nondeterministic +... +SQC045 JSON payload is parsed more than once in one SELECT +XSQCNAME001 default Query reads a temporary table +XSQCSTYLE001 default COUNT(1) should be COUNT(*) +``` + +`default` marks the Rules that run when `[rules] select` is not set. See +[Default Rules](/docs/rules/overview/#default-rules) and the +[Rule reference](/docs/rules/reference/). + +## Showing one Rule + +```bash +sqlclean rules SQC001 +``` + +```text +SQC001: Comparison with NULL is never true +Subject: statement +Enabled by default: yes +Fixable: unsafe +Remediation: Use IS NULL or IS NOT NULL when testing for NULL. +``` + +A custom Rule also shows the Python module that defines it: + +```text +XSQCSTYLE001: COUNT(1) should be COUNT(*) +Subject: statement +Enabled by default: yes +Defined in: sql_rules.naming +Fixable: safe +Remediation: Write COUNT(*). +``` + +`Fixable` is left out when the Rule has no fix. + +## JSON output + +```bash +sqlclean rules --json XSQCNAME001 +``` + +```json +[ + { + "code": "XSQCNAME001", + "message": "Query reads a temporary table", + "remediation": "Read from the published table instead.", + "enabled_by_default": true, + "custom": true, + "subject": "statement", + "module": "sql_rules.naming" + } +] +``` + +| Field | Description | +|-------|-------------| +| `code` | The Rule code | +| `message` | The finding message | +| `remediation` | What to do about a finding | +| `enabled_by_default` | Whether the Rule runs when `[rules] select` is not set | +| `custom` | `true` for custom Rules | +| `subject` | `statement`, `file` or `repository` | +| `fixable` | `safe` or `unsafe`; absent when the Rule has no fix | +| `module` | The Python module of a custom Rule; absent for built-in Rules | + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | The Rules were printed | +| `2` | Unknown Rule code, or the configuration or a custom Rule module could not be loaded | diff --git a/website/src/content/docs/docs/configuration.mdx b/website/src/content/docs/docs/configuration.mdx new file mode 100644 index 0000000..cc0a41f --- /dev/null +++ b/website/src/content/docs/docs/configuration.mdx @@ -0,0 +1,177 @@ +--- +title: "Configuration" +description: "Every sqlclean.toml key, its default, per-path dialects and how the file is found." +--- + +SQLClean reads one `sqlclean.toml`. Every key is optional, and without a file every default +applies. + +```toml +dialect = "duckdb" # default "generic" +include = ["**/*.sql"] +exclude = ["vendor/**"] + +[[dialects]] # a different dialect for some paths +paths = ["warehouse/**"] +dialect = "snowflake" + +[format] +line_width = 100 +indent = 4 + +[rules] +select = ["SQC"] # omit to run the Rules enabled by default +ignore = ["SQC027"] +custom = ["sql_rules"] # Python files or directories that define custom Rules + +[rules.thresholds] +max_literal_length = 100 # SQC044 +max_ranking_order_by = 5 # SQC043 + +[rules.rule_options.XSQCNAME001] +prefixes = ["tmp_", "scratch_"] + +[[rules.rule_exceptions]] # one reviewed finding that is allowed +rule = "SQC038" +path = "models/calendar.sql" +relation = "dates" +reason = "Every pair of dates is needed." + +[[rules.rule_ignores]] # Rules that do not apply to some paths +rules = ["SQC034"] +paths = ["scratch/**"] +reason = "Exploratory queries." +``` + +## Finding the file + +Every command uses the nearest `sqlclean.toml` in the working directory or one of its parents. +`--config ` uses that file instead. + +The directory that holds the file is the **configuration root**. Every path in the file, every path +in findings, and the default set of files to read are relative to it. Without a file, the working +directory is the root. + +## Keys + +| Key | Default | Description | +|-----|---------|-------------| +| `dialect` | `"generic"` | The SQL dialect of every file without a `[[dialects]]` match | +| `include` | `["**/*.sql"]` | Glob patterns of the files to read | +| `exclude` | `[]` | Glob patterns of files never to read | +| `[[dialects]]` | none | `paths` and `dialect`: a different dialect for matching files | +| `[format] line_width` | `100` | The line width the formatter fills; see [Line width](/docs/formatting/#line-width) | +| `[format] indent` | `4` | Spaces per indent step, from `1` to `8`; see [Indent](/docs/formatting/#indent) | +| `[rules] select` | Rules enabled by default | Rule codes or prefixes to run | +| `[rules] ignore` | `[]` | Rule codes or prefixes to skip | +| `[rules] custom` | `[]` | Python files or directories that define [custom Rules](/docs/custom-rules/overview/) | +| `[rules.thresholds] max_literal_length` | `100` | The longest string literal `SQC044` allows | +| `[rules.thresholds] max_ranking_order_by` | `5` | The most `ORDER BY` expressions `SQC043` allows in a ranking window | +| `[rules.rule_options.]` | none | Option values of one custom Rule; see [Options](/docs/custom-rules/fixes-and-options/#options) | +| `[[rules.rule_exceptions]]` | none | One allowed finding; see [Exceptions](/docs/rules/overview/#exceptions) | +| `[[rules.rule_ignores]]` | none | Rules that do not apply to some paths; see [Path ignores](/docs/rules/overview/#path-ignores) | + +Selection, exceptions and ignores are described in [Rules](/docs/rules/overview/). + +## Choosing files + +With no paths on the command line, a command reads every file under the configuration root whose +root-relative path matches `include` and no `exclude` pattern. + +- In patterns, `*` matches within one directory and `**` matches across directories: + `models/*.sql` reads `models/orders.sql` but not `models/marts/orders.sql`. +- In a Git repository, files that `.gitignore` ignores are skipped. +- Hidden files and directories, whose names start with `.`, are skipped. +- A directory on the command line is walked the same way. +- A file named on the command line is read even when `include` does not match it. `exclude` still + applies. + +## Dialects + +`dialect` sets how every file is parsed and formatted. `[[dialects]]` entries set a different +dialect for the files their `paths` match. The first matching entry wins; files that match none use +`dialect`. + +```toml +dialect = "duckdb" + +[[dialects]] +paths = ["reports/**"] +dialect = "tsql" +``` + +A file that does not parse under its dialect is reported as +[`SQC999`](/docs/rules/overview/#parse-errors-sqc999), with the dialect in the message. Without +the `[[dialects]]` entry above, this T-SQL file under `reports/` is parsed as `duckdb`: + +```sql +SELECT [order id] FROM orders +``` + +```text +error[SQC999]: SQL could not be parsed as duckdb: Parse error at line 1, column 17: Expected RBracket, got Var ('id') near [SELECT [order id] FROM orders] +``` + +Supported dialects, case-insensitive: + +| Dialect | Also accepted | +|---------|---------------| +| `generic` | | +| `athena` | | +| `bigquery` | | +| `clickhouse` | | +| `cockroachdb` | `cockroach` | +| `databricks` | | +| `datafusion` | `arrow-datafusion`, `arrow_datafusion` | +| `doris` | | +| `dremio` | | +| `drill` | | +| `druid` | | +| `duckdb` | | +| `dune` | | +| `exasol` | | +| `fabric` | | +| `hana` | `saphana`, `sap_hana` | +| `hive` | | +| `materialize` | | +| `mysql` | | +| `oracle` | | +| `postgres` | `postgresql` | +| `presto` | | +| `redshift` | | +| `risingwave` | | +| `singlestore` | `memsql` | +| `snowflake` | | +| `solr` | | +| `spark` | `spark2` | +| `sqlite` | | +| `starrocks` | | +| `tableau` | | +| `teradata` | | +| `tidb` | | +| `trino` | | +| `tsql` | `mssql`, `sqlserver` | +| `vertica` | | + +## Validation + +SQLClean reads and validates the file when a command starts, and exits with `2` on the first +problem it finds. `check` and `fix` also validate Rule selectors and `rule_options` before they +report any finding. + +| Problem | Error | +|---------|-------| +| A key SQLClean does not know | ``unknown field `colour`, expected one of `dialect`, `include`, ...`` | +| An unknown dialect | `sqlclean.toml: unknown SQL dialect 'nosuch': ...` | +| An invalid glob pattern | `sqlclean.toml: invalid path pattern '[bad': ...` | +| `line_width` of `0` | `sqlclean.toml: [format] line_width must be positive` | +| `indent` of `0` or more than `8` | `sqlclean.toml: [format] indent must be between 1 and 8` | +| A threshold of `0` | `sqlclean.toml: [rules.thresholds] max_literal_length and max_ranking_order_by must be positive` | +| An exception or ignore without a reason | `sqlclean.toml: rule exception or ignore for SQC001 needs a reason` | +| An exception with `cte` but no `relation` | `sqlclean.toml: rule exception for SQC038 at models/calendar.sql sets cte without relation` | +| A `custom` path that does not exist | `[rules] custom path does not exist: sql_rules` | +| `rule_options` for a built-in Rule | `[rules.rule_options.SQC001]: built-in Rules take no options; set limits in [rules.thresholds]` | +| `rule_options` for a code no custom Rule has | `[rules.rule_options.XSQCNOPE001]: no custom Rule has this code` | +| A selector that matches no Rule | `Rule selector 'SQC9' matches no Rule` | + +Errors are printed as `sqlclean: error: `. diff --git a/website/src/content/docs/docs/custom-rules/fixes-and-options.mdx b/website/src/content/docs/docs/custom-rules/fixes-and-options.mdx new file mode 100644 index 0000000..118f69d --- /dev/null +++ b/website/src/content/docs/docs/custom-rules/fixes-and-options.mdx @@ -0,0 +1,154 @@ +--- +title: "Fixes and options" +description: "Give custom Rules safe or unsafe fixes, and typed options configured in sqlclean.toml." +--- + +A custom Rule can attach a fix to each finding, which [`sqlclean fix`](/docs/cli/fix/) applies, +and can declare typed options that a project sets in `sqlclean.toml`. + +## Fixes + +A fix is a list of edits to the file. Declare `fixable` on the Rule, then pass the edits to +`ctx.finding`: + +```python +from sqlclean import FixSafety, Finding, RuleContext, Statement, rule + + +@rule( + code="XSQCSTYLE001", + message="COUNT(1) should be COUNT(*)", + remediation="Write COUNT(*).", + fixable=FixSafety.SAFE, + enabled_by_default=True, +) +def count_one(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + counts = {f.end + 1 for f in statement.functions if f.name.upper() == "COUNT"} + return [ + ctx.finding(subject=statement, node=literal, fix=[ctx.replace(node=literal, text="*")]) + for literal in statement.literals + if literal.text == "1" and literal.start in counts + ] +``` + +A function call is located at its name, so `f.end + 1` is the first character inside its +parentheses. + +```bash +sqlclean fix --diff --select XSQCSTYLE +``` + +```diff +--- a/models/orders.sql ++++ b/models/orders.sql +@@ -1,3 +1,6 @@ +-SELECT customer_id, COUNT(1) AS orders ++SELECT ++ customer_id, ++ COUNT(*) AS orders + FROM customer_orders +-GROUP BY customer_id ++GROUP BY ++ customer_id +Would fix 1 problem(s) and change 1 file(s). +``` + +### Making edits + +| Method | Edit | +|--------|------| +| `ctx.replace(node=..., text=...)` | Replace the node's text | +| `ctx.insert_before(node=..., text=...)` | Insert text just before the node | +| `ctx.insert_after(node=..., text=...)` | Insert text just after the node | +| `ctx.delete(node=...)` | Remove the node's text | + +`node` is any node or statement, or anything with `start` and `end` character offsets in the file. +An `Edit(start=..., end=..., text=...)` from `sqlclean` builds an edit from offsets directly. Every +edit must lie inside the finding's file. + +### Safe and unsafe + +`fixable=FixSafety.SAFE` declares fixes that keep the query's results; `sqlclean fix` applies them. +`fixable=FixSafety.UNSAFE` declares fixes that can change results; they apply only with +`--unsafe-fixes`. The safety belongs to the Rule, so every fix of one Rule has the same safety. +See [Safe and unsafe fixes](/docs/rules/overview/#safe-and-unsafe-fixes). + +A finding without `fix` has no fix, even in a fixable Rule. A Rule that returns a fix without +declaring `fixable` fails with `Rule XSQCSTYLE001 returned a fix but does not declare fixable`. + +### How fixes combine + +`sqlclean fix` applies custom fixes exactly as it applies built-in ones: + +- A fix whose edits overlap another fix's edits waits for the next pass, where the Rule runs again + on the changed file. +- A pass that would leave a file that no longer parses is not applied to that file. +- After at most 10 passes, the files are formatted, so a fix need not produce formatted SQL. + +Keep each edit as small as the change: replace the literal, not the statement. + +## Options + +An option is a typed value a project sets per Rule. Declare it with `RuleOption`, list it in +`options`, and read it with `ctx.option`: + +```python +from sqlclean import Finding, RuleContext, RuleOption, Statement, rule + +PREFIXES = RuleOption.string_list( + name="prefixes", default=("tmp_",), description="Table prefixes that must not be read." +) + + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + options=(PREFIXES,), + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + prefixes = ctx.option(PREFIXES) + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith(prefixes) + ] +``` + +```toml +[rules.rule_options.XSQCNAME001] +prefixes = ["tmp_", "scratch_"] +``` + +`ctx.option` returns the configured value, or the default when `sqlclean.toml` does not set it. +List options arrive as tuples. + +| Constructor | TOML value | Extra arguments | +|-------------|------------|-----------------| +| `RuleOption.boolean(name=, default=, description=)` | `true` or `false` | | +| `RuleOption.integer(name=, default=, description=)` | An integer | `minimum`, `maximum` | +| `RuleOption.string(name=, default=, description=)` | A string | `choices` | +| `RuleOption.string_list(name=, default=, description=)` | A list of strings | | +| `RuleOption.integer_list(name=, default=, description=)` | A list of integers | | + +`name` must be a Python identifier, and the default must be valid for the option. + +### Validation + +Configured values are checked against the declarations, and a bad value stops the command with +exit code `2`: + +| Problem | Error | +|---------|-------| +| A value of the wrong type | `[rules.rule_options.XSQCNAME001] option prefixes expects a list, got 'tmp_'` | +| An integer out of bounds | `[rules.rule_options.XSQCFILE002] option max_statements must be at least 1` | +| A string outside `choices` | `[rules.rule_options.XSQCFILE002] option style must be one of ['strict', 'loose']` | +| A name the Rule does not declare | `Rule XSQCNAME001 has no option prefix` | +| A code no custom Rule has | `[rules.rule_options.XSQCNOPE001]: no custom Rule has this code` | +| A Rule that reads an option it did not declare | `Rule XSQCFILE002 reads undeclared option verbose` | + +Built-in Rules take no options. Their two limits are set in +[`[rules.thresholds]`](/docs/rules/overview/#thresholds). + +Changing an option evaluates that Rule again on every file; cached results for other Rules stay. diff --git a/website/src/content/docs/docs/custom-rules/overview.mdx b/website/src/content/docs/docs/custom-rules/overview.mdx new file mode 100644 index 0000000..e96880b --- /dev/null +++ b/website/src/content/docs/docs/custom-rules/overview.mdx @@ -0,0 +1,193 @@ +--- +title: "Overview" +description: "Write your own Rules as Python functions over the facts of each SQL statement." +--- + +import Card from '../../../../components/docs/Card.astro'; +import CardGroup from '../../../../components/docs/CardGroup.astro'; + +A custom Rule is a Python function decorated with `@rule`. It receives a statement, a file or the +whole repository, and returns findings. Custom Rules use the same selection, exceptions, fixes, +output and cache as the built-in Rules. + +```python +# sql_rules/naming.py +from sqlclean import Finding, RuleContext, Statement, rule + + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith("tmp_") + ] +``` + +Point `[rules] custom` at the file or its directory: + +```toml +[rules] +custom = ["sql_rules"] +``` + +```text +error[XSQCNAME001]: Query reads a temporary table + --> models/orders.sql:2:6 + | + 2 | FROM tmp_orders + | ^^^^^^^^^^ + = help: Read from the published table instead. +``` + +## Where Rules live + +`[rules] custom` lists Python files or directories, relative to the configuration root. SQLClean +imports every `.py` file under them and registers each function decorated with `@rule`. Other +functions, constants and classes stay ordinary Python. + +- Files named `test_*.py`, `conftest.py` and `__init__.py` are not loaded as Rules, so tests can + live next to the Rules they test. +- Modules are imported by their path from the configuration root: `sql_rules/naming.py` is + `sql_rules.naming`. A Rule module can import a helper the same way, for example + `from sql_rules.helpers import table_prefixes`. +- Two Rules with the same code are an error: `custom Rule code XSQCNAME001 is defined twice`. + +## Declaring a Rule + +| `@rule` argument | Default | Description | +|------------------|---------|-------------| +| `code` | required | `XSQC`, optional capital letters, and three digits, such as `XSQCNAME001` | +| `message` | required | The finding message | +| `remediation` | required | What to do about a finding | +| `enabled_by_default` | `False` | Run the Rule when `[rules] select` is not set | +| `fixable` | `None` | `FixSafety.SAFE` or `FixSafety.UNSAFE` when findings carry fixes | +| `options` | `()` | Typed `RuleOption`s configured in `sqlclean.toml` | +| `slug` | the code in lower case | A short name for the Rule | + +A Rule that is not enabled by default runs when `select` names it, by code or by a prefix such as +`XSQC` or `XSQCNAME`. See [Selecting Rules](/docs/rules/overview/#selecting-rules). Fixes and +options are covered in [Fixes and options](/docs/custom-rules/fixes-and-options/). + +## Choosing a subject + +The function takes exactly two keyword-only parameters: one subject and one `RuleContext`. Their +type annotations decide how the Rule is called; their names and order do not matter. + +| Subject | Called | Use it for | +|---------|--------|------------| +| `statement: Statement` | Once per statement | Checks of one query, such as the tables it reads | +| `file: SqlFile` | Once per file | Checks of a file as a whole, such as how many statements it holds | +| `repository: Repository` | Once over every checked file | Conventions across files | + +```python +from collections import Counter + +from sqlclean import Finding, Repository, RuleContext, SqlFile, rule + + +@rule( + code="XSQCFILE001", + message="File holds more than one statement", + remediation="Split the file so each holds one statement.", +) +def one_statement(*, file: SqlFile, ctx: RuleContext) -> list[Finding]: + return [ctx.finding(subject=file)] if len(file.statements) > 1 else [] + + +@rule( + code="XSQCREPO001", + message="Two files define a CTE with the same name", + remediation="Give each CTE a name that is unique across files.", +) +def duplicate_cte_names(*, repository: Repository, ctx: RuleContext) -> list[Finding]: + statements = [s for file in repository.files for s in file.statements] + counts = Counter(cte.name for s in statements for cte in s.ctes) + return [ + ctx.finding(subject=s, node=cte) + for s in statements + for cte in s.ctes + if counts[cte.name] > 1 + ] +``` + +A Repository Rule is evaluated again whenever any checked file changes, so prefer a statement or +file subject when the check needs only one file. + +[Statement facts](/docs/custom-rules/statement-facts/) lists everything a subject exposes. + +## Reporting findings + +A Rule returns a list of findings made with `ctx.finding`: + +| Argument | Description | +|----------|-------------| +| `subject` | The `Statement` or `SqlFile` the finding is in | +| `node` | Where to report it: any node or statement; the start of `subject` when omitted | +| `message` | Replaces the Rule's message for this finding | +| `remediation` | Replaces the Rule's remediation for this finding | +| `fix` | A list of edits; see [Fixes](/docs/custom-rules/fixes-and-options/#fixes) | + +A Repository Rule passes the statement or file each finding belongs to as `subject`. + +## How Rules run + +- Custom Rules run after the built-in Rules, on files that parse. A file with a + [parse error](/docs/rules/overview/#parse-errors-sqc999) gets no custom Rule findings. +- The working directory is the configuration root, and `PYTHONHASHSEED` is `0`, so iterating a set + of strings gives the same order on every run. +- Results are cached with the built-in results. Editing any Python file under `[rules] custom` + evaluates the custom Rules again. See [Caching](/docs/rules/overview/#caching). +- Don't print to standard output from a Rule. The command reads Rule results from it and fails + with `custom Rules host returned no result`. + +## Python environment + +Custom Rules run in a Python interpreter that has the `sqlclean` package installed. SQLClean uses, +in order: + +1. the interpreter named by the `SQLCLEAN_PYTHON` environment variable; +2. the Python installed next to the `sqlclean` executable, such as the one in the same virtual + environment; +3. `python3` on the `PATH`. + +When none has the package, the command fails with: + +```text +Custom Rules need the sqlclean Python package in the environment that runs sqlclean: install it with `pip install sqlclean`, or set SQLCLEAN_PYTHON to an interpreter that has it. +``` + +## Errors + +A Rule that cannot load or run stops the command with exit code `2`: + +| Problem | Error | +|---------|-------| +| An invalid code | `custom Rule code must be XSQC, optional capital letters and 3 digits, such as XSQCNAME001: XSQC01` | +| A wrong signature | `custom Rule XSQCBAD001 must accept exactly one typed Statement, SqlFile or Repository subject and one typed RuleContext, both keyword-only` | +| A module that fails to import | `could not import custom Rule module sql_rules.bad: ` | +| An exception in the Rule | `custom Rule XSQCBAD001 failed on models/a.sql: IndexError: tuple index out of range`, then the traceback | +| A return value that is not a list of findings | `custom Rule XSQCRET001 must return a list of findings from ctx.finding(...)` | +| A fix from a Rule without `fixable` | `Rule XSQCBAD001 returned a fix but does not declare fixable` | + +## Continue + + + + Every node type and field a Rule can read. + + + Add safe or unsafe fixes, and typed options set in `sqlclean.toml`. + + + Test Rules with the `sqlclean.testing` harness. + + + See the facts of any SQL from the command line. + + diff --git a/website/src/content/docs/docs/custom-rules/statement-facts.mdx b/website/src/content/docs/docs/custom-rules/statement-facts.mdx new file mode 100644 index 0000000..3284593 --- /dev/null +++ b/website/src/content/docs/docs/custom-rules/statement-facts.mdx @@ -0,0 +1,169 @@ +--- +title: "Statement facts" +description: "Every node type and field a custom Rule can read from a statement, a file and the repository." +--- + +A custom Rule reads structural facts that the native engine extracts when it parses each file: the +CTEs, tables, joins, stars, function calls, columns and literals of every statement. Every node +carries its position, so a finding can point at it and a fix can replace it. + +```python +from sqlclean import Finding, RuleContext, Statement, rule + + +@rule( + code="XSQCJOIN001", + message="Join to customers has no condition", + remediation="Join customers on customer_id.", +) +def unkeyed_customer_join(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + return [ + ctx.finding(subject=statement, node=join) + for join in statement.joins + if join.relation == "customers" and not join.has_condition + ] +``` + +To see the facts of any SQL, pipe it to [`sqlclean facts`](/docs/cli/facts/). + +## Positions + +Every node, and every statement, has these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `start` | `int` | Offset of the first character in the file | +| `end` | `int` | Offset just past the last character | +| `line` | `int` | Line of `start`, from 1 | +| `column` | `int` | Column of `start`, from 1 | +| `text` | `str` | The source text from `start` to `end` | + +Offsets count Unicode characters from the start of the file, not bytes, so +`file.source[node.start:node.end] == node.text` in Python. Offsets are in the file, not the +statement: the second statement of a file starts after the first. + +## Statement + +One statement of a file, without its terminating semicolon. + +| Field | Type | Description | +|-------|------|-------------| +| `path` | `str` | The file's path relative to the configuration root | +| `index` | `int` | The statement's position in the file, from 0 | +| `kind` | `str` | The statement kind in lower case, such as `select`, `union`, `insert`, `update`, `delete` or `create_table` | +| `dialect` | `str` | The dialect the file was parsed with | +| `ctes` | `tuple[Cte, ...]` | Common table expressions | +| `tables` | `tuple[Table, ...]` | Relations read in `FROM` and `JOIN` | +| `joins` | `tuple[Join, ...]` | Joins | +| `stars` | `tuple[Star, ...]` | `*` and `qualifier.*` projections | +| `functions` | `tuple[FunctionCall, ...]` | Calls of named functions | +| `columns` | `tuple[ColumnReference, ...]` | Column references | +| `literals` | `tuple[Literal, ...]` | String and numeric literals | + +Each tuple is in source order and includes nodes inside CTEs and subqueries. + +`statement.polyglot_ast()` returns the full Polyglot syntax tree of the statement, parsed on first +use. It needs the `ast` extra: `pip install "sqlclean[ast]"`. Without it, the call fails with +`polyglot_ast() needs the Polyglot SQL package: install the ast extra: pip install 'sqlclean[ast]'`. + +## Cte + +A common table expression, located at its name. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The CTE name | + +## Table + +A relation read in `FROM` or `JOIN`, located at its name. A CTE that the query reads appears here +too. The target of `INSERT INTO`, `UPDATE` and `DELETE FROM` does not. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The relation name without its schema, such as `payments` | +| `qualified_name` | `str` | The name as qualified in the SQL, such as `shop.payments` | +| `alias` | `str \| None` | The alias, or `None` | + +## Join + +One join, from its join keyword to the end of its condition, such as +`LEFT JOIN paid ON o.order_id = paid.order_id`. + +| Field | Type | Description | +|-------|------|-------------| +| `kind` | `str` | The join kind in upper case: `INNER` for a bare `JOIN`, `LEFT`, `FULL`, `CROSS`, `NATURAL` and so on | +| `relation` | `str \| None` | The joined relation's name, or `None` when the join reads a subquery | +| `has_condition` | `bool` | Whether the join has `ON` or `USING` | + +A join that holds no table or column reference, such as `CROSS JOIN UNNEST([1, 2])`, is not +listed. + +## Star + +A `*` or `qualifier.*` projection. + +| Field | Type | Description | +|-------|------|-------------| +| `qualifier` | `str \| None` | The qualifier of `o.*`, or `None` for a bare `*` | + +## FunctionCall + +A call of a named function, located at its name. The arguments follow at `end + 1`, after the +opening parenthesis. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The name as written: `count` and `COUNT` differ | + +## ColumnReference + +A column reference, including its qualifier: the text of `o.order_id` is `o.order_id`. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The column name, without quotes | +| `qualifier` | `str \| None` | The relation or alias that qualifies it, or `None` | + +## Literal + +A string or numeric literal as written. `TRUE`, `FALSE` and `NULL` are not literals here. + +| Field | Type | Description | +|-------|------|-------------| +| `kind` | `str` | `string` or `number` | + +`text` holds the literal with its quotes: `'paid'`, `1.5`. + +## SqlFile + +The subject of a file Rule, and the parent of every statement. + +| Field | Type | Description | +|-------|------|-------------| +| `path` | `str` | The path relative to the configuration root | +| `source` | `str` | The file's full text | +| `dialect` | `str` | The dialect the file was parsed with | +| `statements` | `tuple[Statement, ...]` | The statements, in order | + +## Repository + +The subject of a repository Rule. + +| Field | Type | Description | +|-------|------|-------------| +| `files` | `tuple[SqlFile, ...]` | Every checked file that parses, sorted by path | + +## RuleContext + +The second parameter of every Rule. + +| Member | Description | +|--------|-------------| +| `ctx.file` | The `SqlFile` being checked, or `None` in a repository Rule | +| `ctx.dialect` | The dialect of that file, or `None` in a repository Rule | +| `ctx.option(OPTION)` | The configured value of a declared option; see [Options](/docs/custom-rules/fixes-and-options/#options) | +| `ctx.finding(...)` | A finding; see [Reporting findings](/docs/custom-rules/overview/#reporting-findings) | +| `ctx.replace`, `ctx.insert_before`, `ctx.insert_after`, `ctx.delete` | Fix edits; see [Fixes](/docs/custom-rules/fixes-and-options/#fixes) | + +All facts are read-only. A Rule cannot change the SQL except through the edits of a fix. diff --git a/website/src/content/docs/docs/custom-rules/testing.mdx b/website/src/content/docs/docs/custom-rules/testing.mdx new file mode 100644 index 0000000..f1c30ee --- /dev/null +++ b/website/src/content/docs/docs/custom-rules/testing.mdx @@ -0,0 +1,177 @@ +--- +title: "Testing" +description: "Test custom Rules with the sqlclean.testing harness, and keep them deterministic." +--- + +`sqlclean.testing` runs one Rule over SQL you give it, with the same native engine as +`sqlclean check`. It fails when the Rule reports a different number of findings than you expect, +or when its findings depend on the order of the files. + +```python +# sql_rules/test_naming.py +from sqlclean.testing import RuleCase, evaluate_rule + +from sql_rules.naming import count_one, temporary_table + + +def test_scratch_tables_are_reported() -> None: + result = evaluate_rule( + rule=temporary_table, + test_case=RuleCase( + description="reads scratch_orders", + source="SELECT order_id FROM scratch_orders", + dialect="duckdb", + options={"prefixes": ["tmp_", "scratch_"]}, + expected_finding_count=1, + ), + ) + finding = result.findings[0] + assert (finding.start, finding.end) == (21, 35) + + +def test_published_tables_pass() -> None: + evaluate_rule( + rule=temporary_table, + test_case=RuleCase( + description="reads orders", + source="SELECT order_id FROM orders", + expected_finding_count=0, + ), + ) + + +def test_count_one_is_fixed() -> None: + result = evaluate_rule( + rule=count_one, + test_case=RuleCase( + description="COUNT(1)", + source="SELECT COUNT(1) FROM orders", + expected_finding_count=1, + ), + ) + assert result.fixed_source == "SELECT COUNT(*) FROM orders" +``` + +Test failing cases, passing cases and near misses, and assert exact positions where they matter. + +## Running the tests + +Files named `test_*.py` and `conftest.py` under `[rules] custom` are not loaded as Rules, so tests +can live next to the Rules they test. They import Rules by module path from the configuration +root, so put the root on the import path: run `python -m pytest` from the root, add a +`conftest.py` there, or set `pythonpath`: + +```toml +# pyproject.toml +[tool.pytest.ini_options] +pythonpath = ["."] +``` + +The harness runs the `sqlclean` executable installed in the same environment, or the one on the +`PATH`, to parse the case SQL. + +## RuleCase + +| Field | Default | Description | +|-------|---------|-------------| +| `description` | required | Names the case in failure messages | +| `source` | required | The SQL of the file under test | +| `expected_finding_count` | required | How many findings the Rule must report over every file of the case | +| `path` | `"query.sql"` | The path of `source` | +| `dialect` | `"generic"` | The dialect every file of the case is parsed with | +| `files` | `()` | More files, as `RuleFile(path=..., source=...)` | +| `options` | `{}` | Option values, as in `[rules.rule_options.]` | + +`options` are validated as in `sqlclean.toml`, so a test with a bad value fails with the same +error as `sqlclean check`. See [Validation](/docs/custom-rules/fixes-and-options/#validation). + +`files` matter for file and repository Rules. A repository Rule sees every file of the case: + +```python +from sqlclean.testing import RuleCase, RuleFile, evaluate_rule + +from sql_rules.layout import duplicate_cte_names + + +def test_cte_names_repeated_across_files() -> None: + result = evaluate_rule( + rule=duplicate_cte_names, + test_case=RuleCase( + description="paid is defined in two files", + source="WITH paid AS (SELECT order_id FROM payments) SELECT order_id FROM paid", + path="models/paid_orders.sql", + files=( + RuleFile( + path="models/refunds.sql", + source="WITH paid AS (SELECT order_id FROM refunds) SELECT order_id FROM paid", + ), + ), + expected_finding_count=2, + ), + ) + assert [finding.path for finding in result.findings] == [ + "models/paid_orders.sql", + "models/refunds.sql", + ] +``` + +## RuleResult + +| Member | Description | +|--------|-------------| +| `findings` | The findings, sorted by path and position | +| `finding_count` | `len(findings)` | +| `fixed_source` | `source` with the fixes of its findings applied; `source` itself when there are none | + +Each `Finding` has `code`, `path`, `start`, `end`, `message`, `remediation` and `edits`. Offsets +count characters in the file, as in [Statement facts](/docs/custom-rules/statement-facts/#positions). + +`fixed_source` applies every finding's edits once, skipping an edit that overlaps an earlier one, +and does not format the result. It shows exactly what the Rule's fix changes. + +## Failures + +`evaluate_rule` raises `RuleCaseError`, which fails the test: + +| Problem | Message | +|---------|---------| +| A different number of findings | `reads orders: expected 1 findings, got 0` | +| Findings that depend on file order | `custom Rule XSQCND001 is not deterministic` | +| Case SQL that does not parse | `sqlclean could not parse the case SQL: sqlclean: error: Parse error at line 1, column 18: ...` | +| A function without `@rule` | `evaluate_rule needs a function decorated with @rule` | +| No `sqlclean` executable | `the sqlclean executable is not installed; install sqlclean` | + +Errors in the Rule itself, such as an exception or a fix without `fixable`, raise the same errors +as in [`sqlclean check`](/docs/custom-rules/overview/#errors). + +## Determinism + +`sqlclean check` caches each Rule's findings per file and reuses them while the inputs are +unchanged, so a Rule must report the same findings for the same input every time. The harness +checks this: it evaluates the Rule over the case's files, then again in reverse file order, and +fails when the two sets of findings differ. + +The usual cause is state kept between calls, such as a module-level set of names already seen: + +```python +SEEN: set[str] = set() + + +@rule(code="XSQCND001", message="Table read in an earlier file", remediation="...") +def seen_before(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + found = [ctx.finding(subject=statement, node=t) for t in statement.tables if t.name in SEEN] + SEEN.update(t.name for t in statement.tables) + return found +``` + +```text +RuleCaseError: custom Rule XSQCND001 is not deterministic +``` + +Compute findings from the subject and `ctx` alone. When a Rule needs to compare files, give it a +[repository subject](/docs/custom-rules/overview/#choosing-a-subject), which sees every file at +once. + +Avoid other inputs SQLClean does not track, such as the clock, environment variables, network +calls and files read directly from disk. A changed input of that kind does not invalidate cached +results; run with `--no-cache` when it changes. diff --git a/website/src/content/docs/docs/embedding.mdx b/website/src/content/docs/docs/embedding.mdx new file mode 100644 index 0000000..3675c8c --- /dev/null +++ b/website/src/content/docs/docs/embedding.mdx @@ -0,0 +1,147 @@ +--- +title: "Embedding the engine" +description: "Lint, format and read statement facts from Rust, in a tool that owns its own configuration." +--- + +The `sqlclean` crate is the engine without the command: the built-in Rules, the formatter and the +statement facts. Tools that template, compile or generate SQL can call it in process and keep their +own configuration, file discovery and Rule codes. + +```rust +use sqlclean::sql_lint::constants::LINT_API_VERSION; +use sqlclean::sql_lint::main::lint::lint; +use sqlclean::sql_lint::models::{LintRequest, LintResponse}; + +fn main() -> Result<(), String> { + let request = LintRequest { + version: LINT_API_VERSION, + sql: "SELECT order_id FROM orders WHERE cancelled_at = NULL LIMIT 10".to_owned(), + dialect: "duckdb".to_owned(), + enabled_rules: Some(vec!["SQC001".to_owned(), "SQC004".to_owned()]), + ..LintRequest::default() + }; + let response: LintResponse = lint(request)?; + for diagnostic in &response.diagnostics { + println!("{} {}..{} {}", diagnostic.code, diagnostic.start, diagnostic.end, diagnostic.message); + if let Some(fix) = &diagnostic.fix { + println!(" fix: replace {}..{} with {:?}", fix.start, fix.end, fix.replacement); + } + } + Ok(()) +} +``` + +```text +SQC001 47..48 Comparison with NULL is never true + fix: replace 47..48 with "IS" +SQC004 54..59 Row selection is nondeterministic +``` + +## Adding the dependency + +The crate is not published to crates.io. Depend on it from the +[GitHub repository](https://github.com/sqlstack-io/sqlclean), pinned to a release tag or commit: + +```toml +[dependencies] +sqlclean = { git = "https://github.com/sqlstack-io/sqlclean", tag = "" } +``` + +## Entry points + +| Function | What it does | +|----------|--------------| +| `sql_lint::main::lint::lint` | Lints one SQL text with a typed `LintRequest` | +| `sql_lint::main::engine::lint_json` | The same, with the request and response as JSON strings | +| `sql_lint::main::batch_engine::lint_batch_json` | Lints a JSON array of requests in parallel; returns an array of `{"Ok": response}` or `{"Err": message}` | +| `sql_lint::main::formatter::format_json` | Formats SQL from a JSON request | +| `sql_lint::main::catalogue::catalogue` | Every built-in Rule's code, message and remediation | +| `sql_lint::main::default_rules::default_rules` | The codes of the Rules enabled by default | +| `sql_facts::main::file_facts::file_facts` | The statement facts of a SQL text, as custom Rules read them | + +`lint`, `lint_json` and `file_facts` return `Err` with a message when the SQL does not parse under +the dialect. Offsets in requests, findings, edits and facts count Unicode characters, not bytes. + +## Lint requests + +`version` must be `LINT_API_VERSION`; another value is rejected with +`unsupported native lint request version 2; expected 1`. + +| Field | Default | Description | +|-------|---------|-------------| +| `version` | required | `LINT_API_VERSION` | +| `sql` | required | The SQL text | +| `dialect` | required | A [dialect name](/docs/configuration/#dialects) | +| `enabled_rules` | the default Rules | Rule codes or prefixes to run | +| `ignored_rules` | `[]` | Rule codes or prefixes to skip | +| `max_literal_length` | `100` | The `SQC044` limit | +| `max_ranking_order_by` | `5` | The `SQC043` limit | +| `config_file` | `sqlclean.toml` | The configuration file name that remediation text refers to | +| `externally_referenced_ctes` | `[]` | CTE names that something outside the SQL reads, so `SQC005` does not report them | +| `relation_keys` | `{}` | Declared unique keys by relation name; an empty key means the relation holds at most one row, so `SQC038` does not report a cross join with it | + +Other fields describe template code the host has replaced before linting. See `LintRequest` in +`sql_lint::models`. + +A selector that matches no Rule is an error: `unknown native lint rule 'SQC9'`. + +## Fixes + +`LintDiagnostic.fix` holds at most one edit: `start`, `end` and `replacement`. The engine does not +say whether a fix is safe. The `sqlclean` command treats the `SQC001` fix as unsafe, because +turning `= NULL` into `IS NULL` changes which rows match, and every other built-in fix as safe. A +host that applies fixes makes the same decision. See +[Safe and unsafe fixes](/docs/rules/overview/#safe-and-unsafe-fixes). + +## Formatting + +```rust +let request = r#"{"version": 1, "sql": "select order_id, amount from orders where amount > 0", "dialect": "duckdb", "line_width": 100}"#; +println!("{}", format_json(request)?); +``` + +```json +{"version":1,"sql":"SELECT\n order_id,\n amount\nFROM orders\nWHERE amount > 0","changed":true,"formatted":true} +``` + +`indent` sets the spaces per indent step. Without it the engine indents two spaces, its layout +before the `sqlclean` command chose four; pass `"indent": 4` to match `sqlclean format`. + +When the formatter cannot format the SQL, `formatted` is `false`, `sql` is the input unchanged, +and `reason` says why. + +`format_json` formats the text it is given as one unit. `sqlclean format` formats each statement on +its own, joins them with one blank line and ends the file with a newline. To produce the same +layout, split the file with `file_facts`, whose statements carry `start` and `terminated_end`, and +format each statement. See [Formatting](/docs/formatting/#statements). + +## Statement facts + +```rust +let facts = file_facts("SELECT o.order_id FROM orders AS o", "duckdb")?; +for statement in &facts.statements { + for table in &statement.tables { + println!("{} {} {}..{}", statement.kind, table.qualified_name, table.start, table.end); + } +} +``` + +```text +select orders 23..29 +``` + +The fields are those of [Statement facts](/docs/custom-rules/statement-facts/), without `line`, +`column` and `text`. Functions are `NamedNode` values with a `name`. + +## What stays in the host + +The crate reads no files, no configuration and no cache, and does not run Python. A host owns: + +- which files to lint and in which dialect; +- Rule selection, which it passes as `enabled_rules` and `ignored_rules`; +- exceptions, ignores and their reasons; +- the codes and configuration it shows its users; +- caching, and applying fixes. + +The [`sqlclean` command](/docs/cli/check/) is one such host. Its behaviour, described in the rest of +these docs, is a reference for what each of these pieces does. diff --git a/website/src/content/docs/docs/formatting.mdx b/website/src/content/docs/docs/formatting.mdx new file mode 100644 index 0000000..d45b31b --- /dev/null +++ b/website/src/content/docs/docs/formatting.mdx @@ -0,0 +1,186 @@ +--- +title: "Formatting" +description: "One canonical layout for every SQL file, the line width and indent, and files that can't be formatted." +--- + +`sqlclean format` prints every statement in one canonical layout. The only style options are the +line width and the indent, so every file in a project looks the same whoever wrote it. +[`sqlclean fix`](/docs/cli/fix/) formats the files it reads too. + +## Before and after + +```sql +-- Revenue per customer +with paid as (select order_id, customer_id, amount from payments where status = 'paid' and amount > 0), +totals as (select customer_id, sum(amount) as revenue, count(*) as orders from paid group by customer_id) +select c.customer_id, c.customer_name, t.revenue, t.orders, case when t.revenue > 1000 then 'gold' when t.revenue > 100 then 'silver' else 'bronze' end as tier +from customers c left join totals t on c.customer_id = t.customer_id +order by t.revenue desc; +delete from orders where status='cancelled';select 1 +``` + +```bash +sqlclean format +``` + +```sql +-- Revenue per customer +WITH paid AS ( + SELECT + order_id, + customer_id, + amount + FROM payments + WHERE + status = 'paid' AND amount > 0 +), + +totals AS ( + SELECT + customer_id, + SUM(amount) AS revenue, + COUNT(*) AS orders + FROM paid + GROUP BY + customer_id +) + +SELECT + c.customer_id, + c.customer_name, + t.revenue, + t.orders, + CASE + WHEN t.revenue > 1000 + THEN 'gold' + WHEN t.revenue > 100 + THEN 'silver' + ELSE 'bronze' + END AS tier +FROM customers c +LEFT JOIN totals t + ON c.customer_id = t.customer_id +ORDER BY + t.revenue DESC; + +DELETE FROM orders WHERE status = 'cancelled'; + +SELECT 1 +``` + +## The layout + +- Clause keywords and operators such as `SELECT`, `LEFT JOIN`, `AND` and `DESC`, and function + names such as `SUM` and `COUNT`, are upper case. Identifiers, aliases and literals keep the case + they were written in. +- Each clause starts a line. Each indent step is four spaces; see [Indent](#indent). +- A `SELECT` list with two or more items puts one item per line. A single item stays on the + `SELECT` line: `SELECT order_id`, `SELECT *`. +- `GROUP BY` and `ORDER BY` items go on the lines below the clause. +- A `WHERE` with one condition stays on the clause line: `WHERE amount > 0`. Several conditions + go on an indented line below it. +- A join's `ON` condition goes on the next line, indented. +- Each CTE closes with `),` and one blank line. One blank line separates the last CTE from the + query that reads it. +- `CASE` puts each `WHEN`, `THEN` and `ELSE` on its own line. +- Comments are kept. + +## Statements + +Each statement in a file is formatted on its own. Statements are separated by one blank line, and +every file ends with a newline. Semicolons are kept where they are written; the formatter does not +add one after the last statement. + +## Line width + +`line_width` sets the width the formatter fills before it breaks a line. The default is `100`: + +```toml +[format] +line_width = 100 +``` + +A line that does not fit is broken at its outermost structure. Inner parentheses that fit on +their line stay on one line. At the default width this `IN` list fits; at `line_width = 60` it is +broken: + +```sql +SELECT order_id +FROM orders +WHERE status IN ('placed', 'paid', 'packed', 'shipped', 'delivered', 'returned') +``` + +```sql +SELECT order_id +FROM orders +WHERE + status IN ( + 'placed', + 'paid', + 'packed', + 'shipped', + 'delivered', + 'returned' + ) +``` + +`line_width` must be positive. + +## Indent + +`indent` sets the number of spaces per indent step. The default is `4`: + +```toml +[format] +indent = 4 +``` + +With `indent = 2`, `select order_id, amount from orders where amount > 0` formats as: + +```sql +SELECT + order_id, + amount +FROM orders +WHERE amount > 0 +``` + +`indent` must be between `1` and `8`. + +## Idempotency + +Formatting is idempotent: a formatted file formats to itself. Running `sqlclean format` twice +changes nothing the second time, so `sqlclean format --check` passes on any file +`sqlclean format` wrote: + +```text +0 file(s) reformatted, 1 file(s) already formatted. +``` + +## Formatting in CI + +`sqlclean format --check` exits with `1` and names every file that would change, without writing: + +```text +Would reformat: models/open_orders.sql +1 file(s) would be reformatted, 0 file(s) already formatted. +``` + +`--diff` prints the changes instead. See [`sqlclean format`](/docs/cli/format/) and +[Integrations](/docs/integrations/). + +## When a file can't be formatted + +The formatter leaves a file unchanged when it cannot format it, usually because the file does not +parse under its [dialect](/docs/configuration/#dialects). Template code such as `{{ col }}` does +not parse: + +```text +j.sql: could not format: Parse error at line 1, column 16: Expected Colon, got RBrace ('}') near [{{ col }} from orders] +``` + +`sqlclean format` and `sqlclean fix` report the file on stderr and exit with `1`, and format every +other file as usual. Fix the SQL, set the file's dialect with +[`[[dialects]]`](/docs/configuration/#dialects), or +[`exclude`](/docs/configuration/#choosing-files) the file. `sqlclean check` reports the same file as +[`SQC999`](/docs/rules/overview/#parse-errors-sqc999). diff --git a/website/src/content/docs/docs/index.mdx b/website/src/content/docs/docs/index.mdx new file mode 100644 index 0000000..c824b63 --- /dev/null +++ b/website/src/content/docs/docs/index.mdx @@ -0,0 +1,93 @@ +--- +title: "Introduction" +description: "Clean SQL, every time. Fast linting, safe fixes and one canonical format." +--- + +import Card from '../../../components/docs/Card.astro'; +import CardGroup from '../../../components/docs/CardGroup.astro'; + +SQLClean is a free, open-source SQL linter and formatter. It checks `.sql` files against built-in +Rules, applies their fixes, and formats every file in one canonical layout. The engine and the +`sqlclean` command are written in Rust. Custom Rules are plain Python functions that read +structural facts about each statement. + +```bash +sqlclean check +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:1:77 + | + 1 | with open_orders as (select order_id, amount from orders where cancelled_at = null), + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC005]: CTE is unreachable from the final query + --> models/open_orders.sql:2:1 + | + 2 | refunds as (select order_id from payments where amount < 0) + | ^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` + +Found 2 problems (1 fixable with `sqlclean fix`, 1 more with `--unsafe-fixes`). +``` + +`sqlclean fix` removes the unused CTE, then formats the file. The `= NULL` comparison has a fix +too, but it changes which rows match, so it waits for `--unsafe-fixes`. + +## Check + +[`sqlclean check`](/docs/cli/check/) reports findings with the source line, the problem and what to +do about it. Output is text for people, or [JSON and SARIF](/docs/integrations/) for CI and code +scanning. The [built-in Rules](/docs/rules/reference/) cover SQL that runs but returns the wrong +rows: `NULL` comparisons, implicit cartesian joins, `LIMIT` without `ORDER BY`, unused CTEs and +mismatched set operations. Opt-in Rules enforce a consistent query structure. + +## Fix + +[`sqlclean fix`](/docs/cli/fix/) applies every safe fix until the files stop changing, then formats +them. A fix that can change query results is marked unsafe and runs only with `--unsafe-fixes`. +See [Safe and unsafe fixes](/docs/rules/overview/#safe-and-unsafe-fixes). + +## Format + +[`sqlclean format`](/docs/cli/format/) rewrites every file in one canonical layout. Keywords are +upper case, each clause starts a line, and statements are separated by one blank line. Formatting +is idempotent: a formatted file formats to itself. See [Formatting](/docs/formatting/). + +## Extend + +[Custom Rules](/docs/custom-rules/overview/) are Python functions decorated with `@rule`. They read +the tables, joins, CTEs, columns and literals of each statement, can carry fixes and typed options, +and run with the same selection, exceptions and cache as the built-in Rules. Tools that compile or +template SQL can [embed the engine](/docs/embedding/) from Rust. + +## Commands + +| Command | What it does | +|---------|--------------| +| [`sqlclean check`](/docs/cli/check/) | Reports Rule findings without changing any file | +| [`sqlclean fix`](/docs/cli/fix/) | Applies Rule fixes until stable, then formats | +| [`sqlclean format`](/docs/cli/format/) | Rewrites SQL files in the canonical layout | +| [`sqlclean rules`](/docs/cli/rules/) | Lists the available Rules, or shows one | +| [`sqlclean facts`](/docs/cli/facts/) | Prints the statement facts custom Rules read, as JSON | + +## Where to start + + + + Install SQLClean, then check, fix and format your first file. + + + Selection, defaults, exceptions and fixes. + + + Encode your team's SQL conventions in Python. + + + Every command and flag. + + diff --git a/website/src/content/docs/docs/integrations.mdx b/website/src/content/docs/docs/integrations.mdx new file mode 100644 index 0000000..bb6ce92 --- /dev/null +++ b/website/src/content/docs/docs/integrations.mdx @@ -0,0 +1,107 @@ +--- +title: "Integrations" +description: "Run SQLClean in CI with JSON or SARIF output, in pre-commit, and from editors." +--- + +`sqlclean check`, `fix` and `format` exit with `0` when there is nothing to report, `1` when there +are findings or files to change, and `2` when they cannot run. Any CI system, Git hook or editor +can gate on that. + +## CI + +Run `sqlclean check` and `sqlclean format --check` on every change: + +```bash +sqlclean check +sqlclean format --check +``` + +Both read every matching file under the configuration root. `check` fails on findings and +[stale exceptions](/docs/rules/overview/#stale-exceptions); `format --check` fails on files that +need formatting or can't be formatted. + +### Code scanning with SARIF + +`--output-format sarif` writes a SARIF 2.1.0 log that code-scanning tools show as annotations on +the changed lines. On GitHub Actions: + +```yaml +name: sqlclean + +on: [push, pull_request] + +jobs: + sqlclean: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install sqlclean + - run: sqlclean format --check + - run: sqlclean check --output-format sarif > sqlclean.sarif + - uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: sqlclean.sarif +``` + +`sqlclean check` exits with `1` when it reports findings, so the upload step runs with +`if: always()`. The SARIF log lists findings only; stale exceptions still fail the step. See +[SARIF output](/docs/cli/check/#sarif-output). + +### JSON for other tools + +`--output-format json` prints one document with every finding, its position, message, remediation +and fix safety, the stale exceptions, and a summary. See +[JSON output](/docs/cli/check/#json-output). `sqlclean rules --json` describes every Rule a finding +can come from. + +### Caching in CI + +`check` stores results in `.sqlclean/` under the configuration root. Keep that directory between +runs to skip files that have not changed, or pass `--no-cache` to evaluate everything. See +[Caching](/docs/rules/overview/#caching). + +## pre-commit + +SQLClean ships three [pre-commit](https://pre-commit.com/) hooks: + +| Hook | Runs | Changes files | +|------|------|---------------| +| `sqlclean-check` | `sqlclean check` | No | +| `sqlclean-fix` | `sqlclean fix` | Yes: safe fixes, then formatting | +| `sqlclean-format` | `sqlclean format` | Yes: formatting | + +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/sqlstack-io/sqlclean + rev: v0.0.1 + hooks: + - id: sqlclean-fix + - id: sqlclean-format +``` + +Use `sqlclean-check` instead of `sqlclean-fix` where a hook must not change files. + +The hooks run on staged `.sql` files and pass them to the command by name, so `exclude` in +`sqlclean.toml` still applies but `include` does not. They use the nearest `sqlclean.toml` at +or above the repository root. A hook that changes a file, or leaves a finding, fails the commit; +stage the changes and commit again. + +## Editors + +Editors can run `sqlclean format` on save, passing the saved file: + +```bash +sqlclean format path/to/file.sql +``` + +The file is formatted with the dialect, `line_width` and `indent` of the nearest `sqlclean.toml` at or above +the editor's working directory. Use `sqlclean check --output-format json path/to/file.sql` to show +findings with their lines and columns. Lines and columns start at 1 and count characters. diff --git a/website/src/content/docs/docs/quickstart.mdx b/website/src/content/docs/docs/quickstart.mdx new file mode 100644 index 0000000..1702e59 --- /dev/null +++ b/website/src/content/docs/docs/quickstart.mdx @@ -0,0 +1,211 @@ +--- +title: "Quickstart" +description: "Install SQLClean, then check, fix and format your first SQL file." +--- + +This guide installs SQLClean, checks one SQL file, fixes what can be fixed, and adds a +`sqlclean.toml` that selects more Rules. + +## Prerequisites + +- Python 3.12 or newer +- SQLClean installed from [PyPI](https://pypi.org/project/sqlclean/): + +```bash +uv tool install sqlclean # or: pip install sqlclean +sqlclean --version +``` + +The `sqlclean` package installs the native `sqlclean` executable. Custom Rules run in the same +Python environment as that executable. + +## 1. Check a file + +Save this query as `models/open_orders.sql`: + +```sql +with open_orders as (select order_id, customer_id, amount from orders where cancelled_at = null), +refunds as (select order_id from payments where amount < 0) +select order_id, customer_id, amount from open_orders limit 100 +``` + +Run `sqlclean check` in the project directory. With no paths, it reads every `.sql` file under the +directory: + +```bash +sqlclean check +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:1:90 + | + 1 | with open_orders as (select order_id, customer_id, amount from orders where cancelled_at = null), + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC005]: CTE is unreachable from the final query + --> models/open_orders.sql:2:1 + | + 2 | refunds as (select order_id from payments where amount < 0) + | ^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` + +error[SQC004]: Row selection is nondeterministic + --> models/open_orders.sql:3:55 + | + 3 | select order_id, customer_id, amount from open_orders limit 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 3 problems (1 fixable with `sqlclean fix`, 1 more with `--unsafe-fixes`). +``` + +Each finding names its Rule code, the file, line and column, and what to do. The command exits +with `1` because there are findings. + +## 2. Fix + +Preview the safe fixes as a diff, then apply them: + +```bash +sqlclean fix --diff +sqlclean fix +``` + +```text +Fixed 1 problem(s) and changed 1 file(s). +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:7:24 + | + 7 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC004]: Row selection is nondeterministic + --> models/open_orders.sql:15:1 + | + 15 | LIMIT 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 2 problems (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +`fix` removed the unused `refunds` CTE and formatted the file. The `= NULL` fix turns the +comparison into `IS NULL`, which changes which rows match, so it is unsafe. Review it, then apply +it: + +```bash +sqlclean fix --unsafe-fixes +``` + +`SQC004` has no fix: only you know which column makes the order deterministic. Add +`ORDER BY order_id` before `LIMIT 100`, then run `sqlclean fix` once more to format the new +clause. The file is now: + +```sql +WITH open_orders AS ( + SELECT + order_id, + customer_id, + amount + FROM orders + WHERE cancelled_at IS NULL +) + +SELECT + order_id, + customer_id, + amount +FROM open_orders +ORDER BY + order_id +LIMIT 100 +``` + +```bash +sqlclean check +``` + +```text +All checks passed. +``` + +## 3. Format + +`sqlclean fix` formats the files it reads. To format without applying fixes, or to check +formatting in CI, use `sqlclean format`: + +```bash +sqlclean format # rewrite files +sqlclean format --check # exit 1 when a file needs formatting +``` + +See [Formatting](/docs/formatting/) for the layout. + +## 4. Add a sqlclean.toml + +Without a configuration file, SQLClean parses files as `generic` SQL and runs the 13 Rules that +are enabled by default. Create `sqlclean.toml` in the project root to set the dialect and select +every built-in Rule: + +```toml +dialect = "duckdb" + +[rules] +select = ["SQC"] +``` + +`SQC` is a prefix, so it selects all 45 built-in Rules. Three of the opt-in structure Rules now +report the file: + +```text +error[SQC041]: Only the last top-level CTE may be named final, and it must use that name + --> models/open_orders.sql:1:6 + | + 1 | WITH open_orders AS ( + | ^^^^^^^^^^^ + = help: Name the last top-level CTE final and rename any other final CTE; SQC035 checks the terminal SELECT. + +error[SQC034]: Query keeps transformation logic outside top-level CTEs + --> models/open_orders.sql:10:1 + | + 10 | SELECT + | ^^^^^^ + = help: Move transformation logic into named top-level CTEs before the terminal SELECT. + +error[SQC035]: Terminal SELECT does not read plainly from the final top-level CTE + --> models/open_orders.sql:10:1 + | + 10 | SELECT + | ^^^^^^ + = help: Name the final logical CTE and leave one terminal SELECT from that CTE. + +Found 3 problems. +``` + +Follow them, or skip the ones your team does not want: + +```toml +dialect = "duckdb" + +[rules] +select = ["SQC"] +ignore = ["SQC034", "SQC035", "SQC041"] +``` + +```text +All checks passed. +``` + +## Next steps + +- [Configuration](/docs/configuration/) lists every key, including per-path dialects. +- [Rules](/docs/rules/overview/) covers selection, exceptions and fixes, and the + [Rule reference](/docs/rules/reference/) lists every built-in Rule. +- [Integrations](/docs/integrations/) runs SQLClean in CI and pre-commit. +- [Custom Rules](/docs/custom-rules/overview/) checks your own conventions. diff --git a/website/src/content/docs/docs/rules/overview.mdx b/website/src/content/docs/docs/rules/overview.mdx new file mode 100644 index 0000000..c5b3905 --- /dev/null +++ b/website/src/content/docs/docs/rules/overview.mdx @@ -0,0 +1,271 @@ +--- +title: "Overview" +description: "Select Rules, record reviewed exceptions, and apply safe and unsafe fixes." +--- + +import Card from '../../../../components/docs/Card.astro'; +import CardGroup from '../../../../components/docs/CardGroup.astro'; + +A Rule is one check over SQL. It reports a finding at an exact position, with a message, a +remediation, and sometimes a fix. SQLClean has 45 built-in Rules with `SQC###` codes, and runs +[custom Rules](/docs/custom-rules/overview/) written in Python with `XSQC` codes. + +Consider a query that returns an arbitrary hundred rows: + +```sql +SELECT order_id +FROM orders +LIMIT 100 +``` + +```bash +sqlclean check +``` + +```text +error[SQC004]: Row selection is nondeterministic + --> models/recent_orders.sql:3:1 + | + 3 | LIMIT 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 1 problem. +``` + +Add an ordering and check again: + +```sql +SELECT order_id +FROM orders +ORDER BY order_id +LIMIT 100 +``` + +Run `sqlclean rules` for every Rule, or see the [Rule reference](/docs/rules/reference/). + +## Selecting Rules + +Select Rules in `sqlclean.toml` by exact code or by prefix: + +```toml +[rules] +select = ["SQC"] # every built-in Rule +ignore = ["SQC027", "SQC03"] # except SQC027 and SQC030 to SQC039 +``` + +- An exact code such as `SQC004` selects one Rule. +- A prefix selects every Rule whose code starts with it: `SQC` selects all built-in Rules, + `SQC00` selects `SQC001` to `SQC009`, and `XSQCNAME` selects every custom Rule in the `NAME` + family. +- `ignore` removes Rules from the selection, by code or prefix. +- A selector that matches no Rule is an error: `Rule selector 'SQC9' matches no Rule`. + +On the command line, `--select` replaces `[rules] select` for one run, and `--ignore` adds to +`[rules] ignore`: + +```bash +sqlclean check --select SQC001,SQC004 +sqlclean check --ignore SQC039 +``` + +`select = []` runs no Rules. Files are still parsed, so [parse errors](#parse-errors-sqc999) are +still reported. + +## Default Rules + +Without `[rules] select`, SQLClean runs the 13 Rules that are enabled by default. They cover +mistakes that run without an error and are easy to miss in review: NULL handling, joins, row order, +duplicate names and unused CTEs. + +| Code | Finding | +|------|---------| +| `SQC001` | Comparison with NULL is never true | +| `SQC002` | Comma-separated sources create an implicit cartesian join | +| `SQC003` | Non-cross join has no meaningful condition | +| `SQC004` | Row selection is nondeterministic | +| `SQC005` | CTE is unreachable from the final query | +| `SQC006` | DISTINCT is redundant with the grouped output | +| `SQC007` | Positional set operation is vulnerable to column-order drift | +| `SQC009` | Table alias is duplicated in the same query scope | +| `SQC010` | Output alias is duplicated in the same select list | +| `SQC018` | ROW_NUMBER has no deterministic ordering | +| `SQC019` | NOT IN list contains NULL and can reject every row | +| `SQC020` | Set-operation branches project different column counts | +| `SQC039` | Inline query relation obscures data flow | + +The other 32 Rules run only when `select` names them, by code or prefix. A prefix selects every +matching Rule, whether it is enabled by default or not. A custom Rule is enabled by default when its +`@rule` sets `enabled_by_default=True`. + +## Thresholds + +Two built-in Rules compare against a limit, set under `[rules.thresholds]`: + +```toml +[rules.thresholds] +max_literal_length = 100 # SQC044: the longest string literal allowed +max_ranking_order_by = 5 # SQC043: the most ORDER BY expressions in a ranking window +``` + +Both default to the values shown and must be positive. Built-in Rules take no other options. + +## Exceptions + +An exception allows one reviewed finding: one Rule code in one file. It lives in `sqlclean.toml`, +so a reviewer sees it in the diff, and it needs a reason: + +```toml +[[rules.rule_exceptions]] +rule = "SQC004" +path = "models/order_preview.sql" +reason = "A preview of any hundred orders is enough." +``` + +`rule` is an exact code, not a prefix. `path` is the file's path relative to the configuration +root, as findings print it. The exception covers every finding of that Rule in that file. + +SQLClean has no inline suppression comments. A comment such as `-- noqa: SQC001` changes nothing; +record the exception in `sqlclean.toml` instead. + +### Cross join exceptions + +A file-wide exception for a deliberate `CROSS JOIN` would also hide any accidental cross join added +to the file later. `SQC038` findings name the joined relation and, inside a CTE, that CTE, so an +exception can cover exactly one join: + +```text +error[SQC038]: CROSS JOIN with `regions` in CTE `pairs` creates an explicit cartesian product + --> models/calendar.sql:11:3 +``` + +```toml +[[rules.rule_exceptions]] +rule = "SQC038" +path = "models/calendar.sql" +relation = "regions" +cte = "pairs" # optional: only the join inside this CTE +reason = "Every order date is reported for every region." +``` + +`relation` and `cte` compare without case. Without `cte`, the exception covers joins with that +relation anywhere in the file. `cte` needs `relation`, and `relation` matches only `SQC038` +findings. + +`SQC038` does not report a cross join when one side is provably at most one row, such as a CTE +that aggregates without `GROUP BY`, `LIMIT 1` or a one-row `VALUES`, so many intentional cross +joins need no exception: + +```sql +WITH latest AS ( + SELECT MAX(order_date) AS latest_date + FROM orders +) + +SELECT + o.order_id, + l.latest_date +FROM orders AS o +CROSS JOIN latest AS l +``` + +### Stale exceptions + +An exception that matches no finding is stale. `check` and `fix` report it and exit with `1`: + +```text +error: stale rule exception: SQC004 in models/orders.sql matches no finding; remove it from sqlclean.toml +``` + +Delete the entry. Exceptions are only judged stale when their file was checked and their Rule was +selected, so `sqlclean check models/customers.sql` or `--select SQC001` never reports an exception +for another file or Rule. + +## Path ignores + +A path ignore turns Rules off for files that follow a different convention. It takes codes or +prefixes, glob patterns, and a reason: + +```toml +[[rules.rule_ignores]] +rules = ["SQC034", "SQC035"] +paths = ["scratch/**"] +reason = "Exploratory queries." +``` + +Patterns are relative to the configuration root, as in [`include`](/docs/configuration/#choosing-files). +To turn a Rule off everywhere, add it to `[rules] ignore` instead. + +## Parse errors (SQC999) + +A file that does not parse under its [dialect](/docs/configuration/#dialects) gets one `SQC999` +finding and no other: + +```text +error[SQC999]: SQL could not be parsed as duckdb: Parse error at line 1, column 18: Expected table name or subquery, got Where + --> scratch/broken.sql:1:1 + | + 1 | SELECT FROM WHERE orders + | ^ + = help: Fix the syntax error, or set this file's dialect in sqlclean.toml. +``` + +The finding points at the start of the file; the message gives the line and column of the error. +`SQC999` is not a selectable Rule: selection, `ignore`, path ignores and exceptions never hide it. +Fix the SQL, set the right dialect for the file, or [`exclude`](/docs/configuration/#choosing-files) +the file. + +## Safe and unsafe fixes + +Some Rules carry a fix. [`sqlclean fix`](/docs/cli/fix/) applies it. + +- A **safe** fix keeps the query's results. `SQC005` removes an unused CTE; `SQC002` turns + `FROM orders, customers` into `FROM orders CROSS JOIN customers`. +- An **unsafe** fix can change results. `SQC001` turns `cancelled_at = NULL`, which matches no row, + into `cancelled_at IS NULL`, which matches rows. That is almost always the intent, but it is a + different query. Unsafe fixes apply only with `--unsafe-fixes`. + +`sqlclean check` says which command applies each fix: + +```text + = fix: available with `sqlclean fix --unsafe-fixes` +``` + +The [Rule reference](/docs/rules/reference/) lists which Rules can fix and how safely. + +## Caching + +`check` and `fix` reuse the results of earlier runs for inputs that have not changed, so a second +run over an unchanged project costs little more than reading the files. Results are stored in +`.sqlclean/` under the configuration root, which has its own `.gitignore`. + +A file's results are evaluated again when any of these change: + +- the file's text, path or dialect; +- the selected Rules or `[rules.thresholds]`; +- a custom Rule's options, or any Python file under `[rules] custom`; +- for a custom Rule over the whole repository, any checked file; +- the SQLClean version. + +Exceptions and ignores apply after the cache, so editing them takes effect at once. + +Pass `--no-cache` to evaluate every file without reading or writing the cache. Use it when a custom +Rule imports Python from outside its `[rules] custom` paths, since changes there are not tracked. +`sqlclean format` does not use the cache. + +## Continue + + + + Every built-in Rule, its default and its fix. + + + Define your own Rules in Python. + + + Every `sqlclean.toml` key. + + + Flags, output formats and exit codes. + + diff --git a/website/src/content/docs/docs/rules/reference.mdx b/website/src/content/docs/docs/rules/reference.mdx new file mode 100644 index 0000000..6c7eae4 --- /dev/null +++ b/website/src/content/docs/docs/rules/reference.mdx @@ -0,0 +1,262 @@ +--- +title: "Rule reference" +description: "Every built-in Rule, whether it runs by default, and whether it has a fix." +--- + +SQLClean has 45 built-in Rules. `Default` Rules run when `[rules] select` is not set; the others +run when `select` names them by code or prefix. A Rule with a `Safe` fix is fixed by +`sqlclean fix`; an `Unsafe` fix also needs `--unsafe-fixes`. See +[Selecting Rules](/docs/rules/overview/#selecting-rules) and +[Safe and unsafe fixes](/docs/rules/overview/#safe-and-unsafe-fixes). + +Run `sqlclean rules ` for a Rule's remediation. + +| Code | Finding | Default | Fix | +|------|---------|---------|-----| +| `SQC001` | Comparison with NULL is never true | Yes | Unsafe | +| `SQC002` | Comma-separated sources create an implicit cartesian join | Yes | Safe | +| `SQC003` | Non-cross join has no meaningful condition | Yes | Safe | +| `SQC004` | Row selection is nondeterministic | Yes | — | +| `SQC005` | CTE is unreachable from the final query | Yes | Safe | +| `SQC006` | DISTINCT is redundant with the grouped output | Yes | Safe | +| `SQC007` | Positional set operation is vulnerable to column-order drift | Yes | — | +| `SQC008` | Bare UNION leaves duplicate handling implicit | No | Safe | +| `SQC009` | Table alias is duplicated in the same query scope | Yes | — | +| `SQC010` | Output alias is duplicated in the same select list | Yes | — | +| `SQC011` | Clause mixes positional and named references | No | — | +| `SQC012` | ELSE NULL is redundant | No | — | +| `SQC013` | DISTINCT is written as though it were a function | No | Safe | +| `SQC014` | Predicate compares identical constant values | No | — | +| `SQC015` | Empty statement follows another terminator | No | — | +| `SQC016` | Expression is aliased to its existing name | No | — | +| `SQC017` | Row count uses a constant expression | No | Safe | +| `SQC018` | ROW_NUMBER has no deterministic ordering | Yes | — | +| `SQC019` | NOT IN list contains NULL and can reject every row | Yes | — | +| `SQC020` | Set-operation branches project different column counts | Yes | — | +| `SQC021` | Query output has an uncontrolled column shape | No | — | +| `SQC022` | Calculated projection has no stable output name | No | — | +| `SQC023` | Table alias is never referenced | No | — | +| `SQC024` | WHERE predicate rejects NULL rows from a LEFT JOIN | No | — | +| `SQC025` | JOIN type is implicit | No | Safe | +| `SQC026` | ORDER BY mixes explicit and implicit directions | No | — | +| `SQC027` | Column reference is unqualified in a multi-relation query | No | — | +| `SQC028` | Single-relation query mixes qualified and unqualified columns | No | — | +| `SQC029` | Qualified reference uses a relation absent from this query scope | No | — | +| `SQC030` | CASE expression only converts a condition to TRUE or FALSE | No | Safe | +| `SQC031` | CASE expression is nested directly inside ELSE | No | — | +| `SQC032` | Joined relation contributes no referenced values | No | — | +| `SQC033` | Comment is not attached to the syntax node it explains | No | — | +| `SQC034` | Query keeps transformation logic outside top-level CTEs | No | — | +| `SQC035` | Terminal SELECT does not read plainly from the final top-level CTE | No | — | +| `SQC036` | CTE is nested inside another query | No | — | +| `SQC037` | Recursive CTE is not permitted | No | — | +| `SQC038` | CROSS JOIN creates an explicit cartesian product | No | — | +| `SQC039` | Inline query relation obscures data flow | Yes | — | +| `SQC040` | JOIN ON contains a computed expression | No | — | +| `SQC041` | Only the last top-level CTE may be named final, and it must use that name | No | — | +| `SQC042` | CTE output column is never read later in the query | No | — | +| `SQC043` | Ranking window sorts by more expressions than max_ranking_order_by | No | — | +| `SQC044` | String literal is longer than max_literal_length | No | — | +| `SQC045` | JSON payload is parsed more than once in one SELECT | No | — | + +`SQC999` is the [parse error](/docs/rules/overview/#parse-errors-sqc999) finding. It is not a +selectable Rule. + +`SQC043` and `SQC044` read their limits from +[`[rules.thresholds]`](/docs/rules/overview/#thresholds). + +## SQC001: NULL comparisons + +`= NULL` and `<> NULL` are never true, so the filter matches no row: + +```sql +SELECT order_id +FROM orders +WHERE cancelled_at = NULL +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:3:20 + | + 3 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` +``` + +The fix writes `WHERE cancelled_at IS NULL`. It is unsafe because the query then returns rows it +did not return before. + +## SQC002 and SQC003: joins without keys + +A comma between sources joins every row with every row unless a `WHERE` predicate filters them: + +```sql +SELECT + o.order_id, + c.customer_name +FROM orders AS o, customers AS c +WHERE o.customer_id = c.customer_id +``` + +The `SQC002` fix keeps the meaning and makes the cartesian product visible: + +```diff +-FROM orders AS o, customers AS c ++FROM orders AS o ++CROSS JOIN customers AS c + WHERE o.customer_id = c.customer_id +``` + +Move the predicate into `JOIN customers AS c ON o.customer_id = c.customer_id` by hand to make it a +keyed join. `SQC003` reports a `JOIN` whose condition is missing or constant, such as +`ON 1 = 1`; its fix turns a `JOIN` without a condition into `CROSS JOIN`. + +## SQC005: unused CTEs + +A CTE that no reachable query reads is dead code: + +```sql +WITH paid AS ( + SELECT order_id FROM payments +), + +refunded AS ( + SELECT order_id FROM refunds +) + +SELECT order_id FROM paid +``` + +```text +error[SQC005]: CTE is unreachable from the final query + --> models/paid_orders.sql:5:1 + | + 5 | refunded AS ( + | ^^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` +``` + +The fix removes `refunded`. + +## SQC018: unstable row numbers + +Without `ORDER BY` in the window, the row numbered `1` can change between runs: + +```sql +SELECT + order_id, + ROW_NUMBER() OVER (PARTITION BY customer_id) AS order_rank +FROM orders +``` + +```text +error[SQC018]: ROW_NUMBER has no deterministic ordering +``` + +An `ORDER BY` is not always enough. Ties still rank in any order unless `PARTITION BY` and +`ORDER BY` together cover a unique key of the ranked rows. SQLClean only knows a key it can prove +from the query, such as the `GROUP BY` columns of a CTE. Ranking straight over a table is reported +with a second message: + +```text +error[SQC018]: Ranking window order is not proven unique within its partition + --> models/ranked.sql:3:3 + | + 3 | ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at, order_id) AS order_rank + | ^^^^^^^^^^ + = help: PARTITION BY and ORDER BY must together cover a unique key of the ranked rows. End the ORDER BY with a unique, non-null tie-breaker column. If the rows have no unique column, build one upstream from the columns that identify a row (for example `HASH(source_partition, source_offset, line_index) AS row_key`) and order by it last. +``` + +Here the CTE's `GROUP BY` makes `customer_id, order_id` a key, so the window passes: + +```sql +WITH customer_orders AS ( + SELECT + customer_id, + order_id, + MAX(ordered_at) AS ordered_at + FROM orders + GROUP BY + customer_id, + order_id +) + +SELECT + order_id, + ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at, order_id) AS order_rank +FROM customer_orders +``` + +`SQC004` reports the same problem for `LIMIT` and `OFFSET`; see the example in +[Rules](/docs/rules/overview/). + +## SQC019: NULL in NOT IN + +`x NOT IN (..., NULL)` is never true, so the filter rejects every row: + +```sql +SELECT order_id +FROM orders +WHERE status NOT IN ('cancelled', NULL) +``` + +```text +error[SQC019]: NOT IN list contains NULL and can reject every row + --> models/active_orders.sql:3:14 + | + 3 | WHERE status NOT IN ('cancelled', NULL) + | ^^^ + = help: Remove NULL from the list or express the anti-join with a NULL-safe NOT EXISTS. +``` + +## SQC039: inline subqueries + +A subquery in `FROM` hides a step of the query. Name it as a CTE instead: + +```sql +-- Reported +SELECT order_id +FROM (SELECT order_id FROM orders WHERE amount > 0) AS paid + +-- Passes +WITH paid AS ( + SELECT order_id + FROM orders + WHERE amount > 0 +) + +SELECT order_id +FROM paid +``` + +## SQC034, SQC035 and SQC041: query structure + +These opt-in Rules ask every query to do its work in top-level CTEs, name the last one `final`, +and end with a plain `SELECT` of named columns from it. This query passes every built-in Rule: + +```sql +WITH open_orders AS ( + SELECT + order_id, + amount + FROM orders + WHERE cancelled_at IS NULL +), + +final AS ( + SELECT + order_id, + amount + FROM open_orders +) + +SELECT + order_id, + amount +FROM final +``` + +Ending with `SELECT * FROM final` is reported by `SQC021`, which asks for named output columns. diff --git a/website/src/navigation.mjs b/website/src/navigation.mjs new file mode 100644 index 0000000..90f67f0 --- /dev/null +++ b/website/src/navigation.mjs @@ -0,0 +1,5 @@ +import sidebarData from './sidebar.json'; + +export const sidebar = sidebarData; + +export const redirects = {}; diff --git a/website/src/sidebar.json b/website/src/sidebar.json new file mode 100644 index 0000000..301b00c --- /dev/null +++ b/website/src/sidebar.json @@ -0,0 +1,54 @@ +[ + { + "label": "Getting Started", + "items": [ + "docs", + "docs/quickstart" + ] + }, + { + "label": "Concepts", + "items": [ + "docs/configuration", + { + "label": "Rules", + "collapsed": false, + "items": [ + { + "label": "Overview", + "slug": "docs/rules/overview" + }, + "docs/rules/reference" + ] + }, + "docs/formatting" + ] + }, + { + "label": "Custom Rules", + "items": [ + "docs/custom-rules/overview", + "docs/custom-rules/statement-facts", + "docs/custom-rules/fixes-and-options", + "docs/custom-rules/testing" + ] + }, + { + "label": "Integrations", + "items": [ + "docs/integrations", + "docs/embedding", + "docs/benchmarks" + ] + }, + { + "label": "CLI Reference", + "items": [ + "docs/cli/check", + "docs/cli/fix", + "docs/cli/format", + "docs/cli/rules", + "docs/cli/facts" + ] + } +] diff --git a/website/src/styles/fonts.css b/website/src/styles/fonts.css new file mode 100644 index 0000000..566aaec --- /dev/null +++ b/website/src/styles/fonts.css @@ -0,0 +1,42 @@ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 100 900; + font-display: swap; + src: url('/fonts/inter-latin-wght-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-400-normal.woff2') format('woff2'); +} + +@font-face { + font-family: 'IBM Plex Mono'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('/fonts/ibm-plex-mono-latin-500-normal.woff2') format('woff2'); +} + +/* IBM Plex Mono has no box-drawing glyphs. DejaVu Sans Mono fills them in, and draws whole + diagrams so every cell has the same advance width. */ +@font-face { + font-family: 'SQLClean Box Drawing'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); + unicode-range: U+2500-259F, U+2260; +} + +@font-face { + font-family: 'SQLClean Diagram'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('/fonts/dejavu-sans-mono-diagram.woff2') format('woff2'); +} diff --git a/website/src/styles/sqlclean.css b/website/src/styles/sqlclean.css new file mode 100644 index 0000000..701075d --- /dev/null +++ b/website/src/styles/sqlclean.css @@ -0,0 +1,446 @@ +/* Starlight theme mapped onto the homepage tokens in public/styles.css. */ +:root { + --sl-font: 'Inter', system-ui, sans-serif; + --sl-font-mono: 'IBM Plex Mono', 'SQLClean Box Drawing', ui-monospace, monospace; + --sl-nav-height: 60px; + --sl-content-width: 50rem; + --sl-sidebar-width: 18rem; + --sqc-toc-width: 16rem; + --sqc-frame-width: 1440px; + --sl-text-h1: 2rem; + --sl-text-h2: 1.45rem; + --sl-text-h3: 1.15rem; + --sl-text-h4: 1rem; + --sl-line-height-headings: 1.25; + + /* Dark (Starlight's default) */ + --sqc-bg: #0d0f12; + --sqc-panel: #171a1f; + --sqc-fg: #e9ebef; + --sqc-muted: #9ba1ac; + --sqc-faint: #626975; + --sqc-line: rgba(176, 184, 198, 0.1); + --sqc-line-strong: rgba(176, 184, 198, 0.18); + --sqc-primary: #1fd8b8; + --sqc-accent-soft: rgba(31, 216, 184, 0.12); + --sqc-code-bg: #111418; + --sqc-warning: #f0c267; + + --sl-color-accent-low: #0b3a33; + --sl-color-accent: #1fd8b8; + --sl-color-accent-high: #a6f2e4; + --sl-color-white: #e9ebef; + --sl-color-gray-1: #d4d7dd; + --sl-color-gray-2: #b4b9c2; + --sl-color-gray-3: #9ba1ac; + --sl-color-gray-4: #626975; + --sl-color-gray-5: #2a2f37; + --sl-color-gray-6: #171a1f; + --sl-color-black: #0d0f12; +} + +:root[data-theme='light'] { + --sqc-bg: #ffffff; + --sqc-panel: #ffffff; + --sqc-fg: #171a1f; + --sqc-muted: #626a76; + --sqc-faint: #98a0ad; + --sqc-line: rgba(28, 32, 38, 0.08); + --sqc-line-strong: rgba(28, 32, 38, 0.16); + --sqc-primary: #0b8a74; + --sqc-accent-soft: rgba(11, 138, 116, 0.08); + --sqc-code-bg: #fbfcfe; + --sqc-warning: #aa6c16; + + --sl-color-accent-low: #dff6f1; + --sl-color-accent: #0b8a74; + --sl-color-accent-high: #075a4c; + --sl-color-white: #171a1f; + --sl-color-gray-1: #2b3038; + --sl-color-gray-2: #434a55; + --sl-color-gray-3: #626a76; + --sl-color-gray-4: #98a0ad; + --sl-color-gray-5: #dfe3e8; + --sl-color-gray-6: #f4f6f8; + --sl-color-gray-7: #fafbfc; + --sl-color-black: #ffffff; +} + +:root { + --sl-color-bg: var(--sqc-bg); + --sl-color-bg-nav: var(--sqc-bg); + --sl-color-bg-sidebar: var(--sqc-bg); + --sl-color-bg-inline-code: var(--sqc-accent-soft); + --sl-color-hairline: var(--sqc-line); + --sl-color-hairline-light: var(--sqc-line); + --sl-color-hairline-shade: var(--sqc-line); + --sl-color-text: var(--sqc-fg); + --sl-color-text-accent: var(--sqc-primary); +} + +body { + -webkit-font-smoothing: antialiased; +} + +/* Header */ +site-search button[data-open-modal] { + border-radius: 2px; + border-color: var(--sqc-line-strong); + background: transparent; + color: var(--sqc-muted); +} + +site-search button[data-open-modal]:hover { + border-color: var(--sqc-primary); + color: var(--sqc-fg); +} + +/* Sidebar: bold top-level headings, nested groups styled like links (as in common docs sites). */ +.sidebar-content ul ul li { + margin-inline-start: 0; + padding-inline-start: 0; + border-inline-start: 0; +} + +.sidebar-content .top-level > li + li { + margin-top: 1.1rem; +} + +.sidebar-content .top-level > li > details > summary { + padding: 0.2rem 0.5rem 0.35rem; + pointer-events: none; +} + +.sidebar-content .top-level > li > details > summary .caret { + display: none; +} + +.sidebar-content .top-level > li > details > summary .large { + color: var(--sqc-fg); + font-size: 14px; + font-weight: 600; +} + +.sidebar-content ul ul summary { + padding: 0.3rem 0.5rem; + border-radius: 2px; +} + +.sidebar-content ul ul summary:hover { + background: var(--sqc-accent-soft); +} + +.sidebar-content ul ul summary .large { + color: var(--sqc-muted); + font-size: 14px; + font-weight: 400; + line-height: 1.4; +} + +.sidebar-content ul ul summary:hover .large { + color: var(--sqc-fg); +} + +.sidebar-content ul ul summary .caret { + width: 1rem; + height: 1rem; + color: var(--sqc-faint); +} + +.sidebar-content ul ul ul { + margin-inline-start: 0.75rem; +} + +.sidebar-content a { + padding: 0.3rem 0.5rem; + border-radius: 2px; + font-size: 14px; + line-height: 1.4; + color: var(--sqc-muted); +} + +.sidebar-content a:hover { + background: var(--sqc-accent-soft); + color: var(--sqc-fg); +} + +.sidebar-content a[aria-current='page'], +.sidebar-content a[aria-current='page']:hover { + background: var(--sqc-accent-soft); + color: var(--sqc-primary); + font-weight: 500; +} + +/* Page title and on-this-page */ +.content-panel { + border-top-color: var(--sqc-line); +} + +h1#_top { + font-weight: 650; + letter-spacing: -0.02em; +} + +.sl-markdown-content :is(h2, h3, h4) { + font-weight: 620; + letter-spacing: -0.01em; + color: var(--sqc-fg); +} + +.sl-markdown-content .sl-heading-wrapper.level-h2 { + margin-top: 2.75rem; +} + +/* Title and body share one panel, with no dividing line. */ +.content-panel + .content-panel { + border-top: 0; + padding-top: 0; +} + +.content-panel:first-of-type { + padding-bottom: 2.25rem; +} + +.sl-markdown-content p, +.sl-markdown-content li { + color: var(--sqc-muted); +} + +.sl-markdown-content strong { + color: var(--sqc-fg); + font-weight: 600; +} + +starlight-toc h2 { + margin-bottom: 0.5rem; + color: var(--sqc-fg); + font-size: 13px; + font-weight: 600; +} + +starlight-toc li:has(> a[href='#_top']) { + display: none; +} + +starlight-toc a, +mobile-starlight-toc a { + padding-block: 0.3rem; + font-size: 13.5px; + line-height: 1.4; + color: var(--sqc-muted); +} + +starlight-toc a:hover { + color: var(--sqc-fg); +} + +starlight-toc a[aria-current='true'], +starlight-toc a[aria-current='true']:hover { + color: var(--sqc-primary); + font-weight: 500; +} + +/* Links */ +.sl-markdown-content a:not(.sqc-card) { + color: var(--sqc-fg); + font-weight: 500; + text-decoration: underline; + text-decoration-color: var(--sqc-primary); + text-decoration-thickness: 1px; + text-underline-offset: 3px; +} + +.sl-markdown-content a:not(.sqc-card):hover { + color: var(--sqc-primary); + text-decoration-thickness: 2px; +} + +/* Code blocks: a quiet copy icon that is always visible, as in common docs sites. */ +.expressive-code .copy button { + opacity: 0.6 !important; +} + +.expressive-code .copy button:hover { + opacity: 1 !important; +} + +/* Inline code */ +.sl-markdown-content :not(pre) > code { + border: 0; + border-radius: 2px; + padding: 0.1em 0.35em; + font-size: 0.86em; + color: var(--sqc-fg); +} + +/* Tables */ +.sl-markdown-content table { + display: table; + width: 100%; + border-collapse: collapse; + font-size: 14px; + line-height: 1.5; +} + +@media (max-width: 50rem) { + .sl-markdown-content table { + display: block; + overflow-x: auto; + } +} + +.sl-markdown-content :is(th, td) { + border: 0; + border-bottom: 1px solid var(--sqc-line); + padding: 0.55rem 0.9rem 0.55rem 0; + vertical-align: top; +} + +.sl-markdown-content th { + background: transparent; + color: var(--sqc-fg); + font-weight: 600; + white-space: nowrap; + border-bottom-color: var(--sqc-line-strong); +} + +.sl-markdown-content tr:nth-child(2n) { + background: transparent; +} + +.sl-markdown-content td { + color: var(--sqc-muted); +} + +.sl-markdown-content td:first-child code { + white-space: nowrap; +} + +/* Search */ +site-search { + --pagefind-ui-border-radius: 2px; +} + +site-search dialog { + border-radius: 2px; +} + +/* Callouts */ +.sqc-callout { + display: flex; + gap: 12px; + padding: 14px 16px; + border: 1px solid color-mix(in srgb, var(--sqc-callout) 30%, transparent); + border-radius: 2px; + background: color-mix(in srgb, var(--sqc-callout) 8%, transparent); + font-size: 14px; + line-height: 1.6; +} + +.sqc-callout.note { + --sqc-callout: var(--sqc-primary); +} + +.sqc-callout.warning { + --sqc-callout: var(--sqc-warning); +} + +.sl-markdown-content .sqc-callout-icon { + width: 16px; + height: 16px; +} + +.sl-markdown-content .sqc-callout > .sqc-callout-body { + margin: 0; +} + +.sqc-callout-icon { + flex: none; + margin-top: 4px; + color: var(--sqc-callout); +} + +.sl-markdown-content .sqc-callout-body > * { + margin: 0; + color: var(--sqc-fg); +} + +.sl-markdown-content .sqc-callout-body > * + * { + margin-top: 0.6rem; +} + +/* Docs components: Card, CardGroup, Frame, Note and Warning */ +.sqc-card-group { + display: grid; + grid-template-columns: repeat(var(--cols, 2), minmax(0, 1fr)); + gap: 12px; + margin-top: 1.25rem; +} + +@media (max-width: 50rem) { + .sqc-card-group { + grid-template-columns: 1fr; + } +} + +.sl-markdown-content .sqc-card { + display: flex; + flex-direction: column; + margin: 0; + padding: 18px 20px; + border: 1px solid var(--sqc-line-strong); + border-radius: 2px; + background: var(--sqc-panel); + color: var(--sqc-muted); + text-decoration: none; + font-size: 14px; + line-height: 1.5; +} + +.sl-markdown-content a.sqc-card:hover { + border-color: var(--sqc-primary); +} + +.sqc-card-icon { + margin-bottom: 12px; + color: var(--sqc-primary); +} + +.sqc-card-title { + color: var(--sqc-fg); + font-weight: 600; + font-size: 15px; +} + +.sqc-card-body { + margin-top: 4px; +} + +.sqc-card-body > * { + margin: 0; +} + +.sqc-frame { + margin: 1rem 0; + padding: 0; + border: 1px solid var(--sqc-line); +} + +.sqc-frame img { + display: block; + width: 100%; +} + +/* Mobile controls */ +.sl-menu-button { + border: 1px solid var(--sqc-line-strong) !important; + border-radius: 2px !important; + background: var(--sqc-bg) !important; + color: var(--sqc-muted) !important; + box-shadow: none !important; +} + +mobile-starlight-toc summary .toggle { + border-radius: 2px; + border-color: var(--sqc-line-strong); +} From 9dba1fd448f1b72a0626ec36f4872aadb0534fc0 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 18:57:25 +0000 Subject: [PATCH 15/29] chore(bench): compare SQLClean with sqruff and SQLFluff bench.sh runs each linter on the plain-SQL DuckShop 10K corpus from SQLBuild's generator, cold and, for SQLClean, warm, and summarize.py reports median wall time, CPU time, peak memory and finding counts. convert_duckshop.py turns SQLBuild's compiled models into plain Snowflake SQL. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- benchmarks/linters/README.md | 48 +++++++++++++ benchmarks/linters/bench.sh | 91 +++++++++++++++++++++++++ benchmarks/linters/convert_duckshop.py | 73 ++++++++++++++++++++ benchmarks/linters/summarize.py | 93 ++++++++++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 benchmarks/linters/README.md create mode 100755 benchmarks/linters/bench.sh create mode 100644 benchmarks/linters/convert_duckshop.py create mode 100644 benchmarks/linters/summarize.py diff --git a/benchmarks/linters/README.md b/benchmarks/linters/README.md new file mode 100644 index 0000000..a3751b5 --- /dev/null +++ b/benchmarks/linters/README.md @@ -0,0 +1,48 @@ +# Linter benchmark + +SQLClean, sqruff and SQLFluff linting the same 10,282 Snowflake SQL files. The corpus is SQLBuild's +DuckShop 10K project with the SQLBuild-specific parts removed, so every tool reads plain SQL. + +## Build the corpus + +The corpus comes from SQLBuild's `benchmarks/duckshop-10k` generator (generator version 2, SQLBuild +commit `def64244c`). Compiling expands its macros and drops its model headers; `convert_duckshop.py` +then replaces SQLBuild's reference functions, such as `__ref("orders")`, with plain names. + +```bash +python sqlbuild/benchmarks/duckshop-10k/generate.py --out /tmp/duckshop-10k +(cd /tmp/duckshop-10k && sqb compile) +python benchmarks/linters/convert_duckshop.py /tmp/duckshop-10k/target/compiled/models /tmp/corpus +``` + +`/tmp/corpus/sql` then holds 10,282 `.sql` files (44 MB) and one configuration per tool, each +selecting the Snowflake dialect and nothing else, so every tool runs its default rules. + +## Run + +```bash +pip install sqruff sqlfluff +benchmarks/linters/bench.sh /tmp/corpus/sql /tmp/linter-bench 3 +python benchmarks/linters/summarize.py /tmp/linter-bench +``` + +Each round copies the corpus into a fresh directory, so no tool starts with a cache, and runs every +variant in alternating order. SQLClean variants run a second time with nothing changed to measure +its result cache; sqruff and SQLFluff have no result cache. `bench.sh` records wall time, CPU time +and peak memory with `/usr/bin/time`, and every lint command writes JSON findings to stdout. + +| Variant | Command | +|---|---| +| `sqlclean` | `sqlclean check --output-format json` (13 default Rules) | +| `sqlclean_all` | `sqlclean check --select SQC --output-format json` (all 45 Rules) | +| `sqlclean_format` | `sqlclean format --check` | +| `sqruff` | `sqruff lint -f json .` | +| `sqlfluff` | `sqlfluff lint -f json --processes $(nproc) --disable-progress-bar .` | + +## Reading the results + +The tools do not check the same things. Almost every sqruff and SQLFluff finding on this corpus +comes from a layout rule (`LT01`–`LT14`), which SQLClean leaves to its formatter. The closest +like-for-like comparison is therefore `sqlclean check` plus `sqlclean format --check` against the +other tools' default `lint`. Counts of findings are reported to show that every tool read the +corpus, not to compare how many problems each finds. diff --git a/benchmarks/linters/bench.sh b/benchmarks/linters/bench.sh new file mode 100755 index 0000000..a0bc690 --- /dev/null +++ b/benchmarks/linters/bench.sh @@ -0,0 +1,91 @@ +#!/usr/bin/env bash +# Linter benchmark: SQLClean vs sqruff vs SQLFluff on the plain-SQL DuckShop 10K corpus. +# +# Usage: bench.sh CORPUS OUTDIR ROUNDS [VARIANT...] +# +# CORPUS is OUTDIR/sql from convert_duckshop.py: the SQL files plus a configuration for each linter +# that selects the Snowflake dialect. Each round runs every variant once, in alternating order (odd +# rounds forward, even rounds reversed). For each variant and round: +# 1. copy the pristine corpus into a work directory (no caches) +# 2. cold: lint every file +# 3. warm: lint again with nothing changed (only for variants listed in WARM_VARIANTS) +# +# Every tool writes JSON findings to stdout, so each run also pays for its own reporting. +# +# Environment: +# SQLCLEAN sqlclean executable (default: sqlclean) +# SQRUFF sqruff executable (default: sqruff) +# SQLFLUFF sqlfluff executable (default: sqlfluff) +# PROCESSES SQLFluff worker processes (default: nproc). SQLClean and sqruff pick their own. +# +# Default variants: sqlclean sqlclean_all sqlclean_format sqruff sqlfluff. +set -u +CORPUS=$(realpath "$1") +OUT=$(realpath -m "$2") +ROUNDS=$3 +shift 3 +SQLCLEAN=${SQLCLEAN:-sqlclean} +SQRUFF=${SQRUFF:-sqruff} +SQLFLUFF=${SQLFLUFF:-sqlfluff} +PROCESSES=${PROCESSES:-$(nproc)} +WORK=$(mktemp -d) +trap 'rm -rf "$WORK"' EXIT + +declare -A CMD +CMD[sqlclean]="$SQLCLEAN check --output-format json" +CMD[sqlclean_all]="$SQLCLEAN check --select SQC --output-format json" +# sqruff's and SQLFluff's default rules include layout; SQLClean checks layout with its formatter. +CMD[sqlclean_format]="$SQLCLEAN format --check" +CMD[sqruff]="$SQRUFF lint -f json ." +CMD[sqlfluff]="$SQLFLUFF lint -f json --processes $PROCESSES --disable-progress-bar ." +# SQLClean reuses results for unchanged files; the others have no result cache. +WARM_VARIANTS=" sqlclean sqlclean_all " + +VARIANTS=("$@") +if [ ${#VARIANTS[@]} -eq 0 ]; then + VARIANTS=(sqlclean sqlclean_all sqlclean_format sqruff sqlfluff) +fi +for v in "${VARIANTS[@]}"; do + if [ -z "${CMD[$v]+set}" ]; then + echo "unknown variant: $v" >&2 + exit 2 + fi +done +mkdir -p "$OUT" +{ + echo "files $(find "$CORPUS" -name '*.sql' | wc -l)" + echo "nproc $(nproc)" + echo "processes $PROCESSES" + "$SQLCLEAN" --version + "$SQRUFF" --version + "$SQLFLUFF" --version +} >"$OUT/environment.txt" 2>&1 + +load1() { cut -d' ' -f1 /proc/loadavg; } + +measure() { + local v=$1 scenario=$2 round=$3 base before after + base="$OUT/$v.$scenario.r$round" + echo "${CMD[$v]}" >"$base.cmd" + before=$(load1) + /usr/bin/time -f '%e %U %S %M' -o "$base.time" \ + bash -c "cd '$WORK/$v' && exec ${CMD[$v]}" >"$base.out" 2>"$base.err" + echo $? >"$base.rc" + after=$(load1) + echo "$v $scenario r$round load $before $after" >>"$OUT/uptime.log" + echo "$v $scenario r$round rc=$(cat "$base.rc") time=$(tail -1 "$base.time") load=$before/$after" | + tee -a "$OUT/summary.log" +} + +for round in $(seq 1 "$ROUNDS"); do + n=${#VARIANTS[@]} + for i in $(seq 0 $((n - 1))); do + if [ $((round % 2)) -eq 0 ]; then v=${VARIANTS[$((n - 1 - i))]}; else v=${VARIANTS[$i]}; fi + rm -rf "${WORK:?}/$v" + cp -a "$CORPUS" "$WORK/$v" + sync + measure "$v" cold "$round" + [[ $WARM_VARIANTS == *" $v "* ]] && measure "$v" warm "$round" + rm -rf "${WORK:?}/$v" + done +done diff --git a/benchmarks/linters/convert_duckshop.py b/benchmarks/linters/convert_duckshop.py new file mode 100644 index 0000000..7fc7839 --- /dev/null +++ b/benchmarks/linters/convert_duckshop.py @@ -0,0 +1,73 @@ +"""Turn SQLBuild's compiled DuckShop 10K models into plain Snowflake SQL files. + +Usage: python convert_duckshop.py COMPILED_MODELS OUTDIR + +COMPILED_MODELS is `target/compiled/models` after `sqb compile` in a project written by SQLBuild's +`benchmarks/duckshop-10k/generate.py`. Compiling has already removed the MODEL headers and expanded +the macros; what is left of SQLBuild is its reference functions, which become ordinary names: + +| Compiled SQL | Plain SQL | +|---|---| +| `__ref("orders")` | `orders` | +| `__source("ledger__journals")` | `raw.ledger__journals` | +| `__seed("source_instances")` | `seeds.source_instances` | +| `__udf("luhn_valid")` | `luhn_valid` | +| `__table_fn("customer_orders")(...)` | `TABLE(customer_orders(...))` | +| `__cursor_start()`, `__cursor_end()` | fixed timestamps | + +OUTDIR/sql/ receives one file per model in the same folders, plus a configuration for each linter +that selects the Snowflake dialect and nothing else, so each tool runs its default Rules. +""" + +import argparse +import re +from pathlib import Path + +REPLACEMENTS: tuple[tuple[re.Pattern[str], str], ...] = ( + (re.compile(r'__table_fn\("([^"]+)"\)\(([^()]*)\)'), r"TABLE(\1(\2))"), + (re.compile(r'__ref\("([^"]+)"\)'), r"\1"), + (re.compile(r'__source\("([^"]+)"\)'), r"raw.\1"), + (re.compile(r'__seed\("([^"]+)"\)'), r"seeds.\1"), + (re.compile(r'__udf\("([^"]+)"\)'), r"\1"), + (re.compile(r"__cursor_start\(\)"), "'2026-01-01 00:00:00'::TIMESTAMP_NTZ"), + (re.compile(r"__cursor_end\(\)"), "'2026-01-02 00:00:00'::TIMESTAMP_NTZ"), +) +LEFTOVER: re.Pattern[str] = re.compile(r"\b__[a-z_]+\(") +CONFIGS: dict[str, str] = { + "sqlclean.toml": 'dialect = "snowflake"\n', + ".sqruff": "[sqruff]\ndialect = snowflake\n", + ".sqlfluff": "[sqlfluff]\ndialect = snowflake\n", +} + + +def plain_sql(text: str) -> str: + """Return text with every SQLBuild reference function replaced by plain SQL.""" + + for pattern, replacement in REPLACEMENTS: + text = pattern.sub(replacement, text) + leftover: re.Match[str] | None = LEFTOVER.search(text) + if leftover is not None: + raise SystemExit(f"unconverted SQLBuild function: {leftover.group(0)}") + return text + + +def main() -> None: + """Convert every compiled model and write the linter configurations.""" + + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("compiled", type=Path) + parser.add_argument("out", type=Path) + arguments = parser.parse_args() + sql: Path = arguments.out / "sql" + files: list[Path] = sorted(arguments.compiled.rglob("*.sql")) + for source in files: + target: Path = sql / source.relative_to(arguments.compiled) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(plain_sql(source.read_text(encoding="utf-8")), encoding="utf-8") + for name, text in CONFIGS.items(): + (sql / name).write_text(text, encoding="utf-8") + print(f"wrote {len(files)} SQL files to {sql}") + + +if __name__ == "__main__": + main() diff --git a/benchmarks/linters/summarize.py b/benchmarks/linters/summarize.py new file mode 100644 index 0000000..c77d8b1 --- /dev/null +++ b/benchmarks/linters/summarize.py @@ -0,0 +1,93 @@ +"""Summarize bench.sh output: median wall, CPU, peak memory and findings per variant and scenario. + +Usage: python summarize.py OUTDIR +""" + +import json +import re +import statistics +import sys +from collections import defaultdict +from pathlib import Path + +SCENARIOS: tuple[str, ...] = ("cold", "warm") +VARIANT_ORDER: tuple[str, ...] = ( + "sqlclean", + "sqlclean_all", + "sqlclean_format", + "sqruff", + "sqlfluff", +) +LABELS: dict[str, str] = { + "sqlclean": "SQLClean, default Rules", + "sqlclean_all": "SQLClean, every built-in Rule", + "sqlclean_format": "SQLClean formatter, `format --check`", + "sqruff": "sqruff, default rules", + "sqlfluff": "SQLFluff, default rules", +} +TIME_RE: re.Pattern[str] = re.compile( + r"^(?P\w+)\.(?P\w+)\.r(?P\d+)\.time$" +) + + +def count_findings(variant: str, out: str) -> tuple[int, int]: + """Return (findings, parse errors) from a tool's JSON output.""" + + if variant == "sqlclean_format": + return out.count("Would reformat: "), 0 + data = json.loads(out) + if variant.startswith("sqlclean"): + codes = [finding["code"] for finding in data["findings"]] + return len(codes), codes.count("SQC999") + if variant == "sqruff": + codes = [finding.get("code") for findings in data.values() for finding in findings] + return len(codes), sum(1 for code in codes if code in (None, "PRS")) + codes = [violation["code"] for file in data for violation in file["violations"]] + return len(codes), sum(1 for code in codes if code in ("PRS", "TMP")) + + +def main() -> None: + """Print one Markdown table row per variant and scenario.""" + + out = Path(sys.argv[1]) + runs: dict[tuple[str, str], list[tuple[float, float, int, int]]] = defaultdict(list) + findings: dict[tuple[str, str], set[tuple[int, int]]] = defaultdict(set) + for path in sorted(out.glob("*.time")): + match = TIME_RE.match(path.name) + if match is None: + continue + stem = str(path)[: -len(".time")] + wall, user, system, rss = path.read_text().strip().splitlines()[-1].split() + rc = int(Path(stem + ".rc").read_text()) + key = (match["variant"], match["scenario"]) + runs[key].append((float(wall), float(user) + float(system), int(rss), rc)) + findings[key].add(count_findings(match["variant"], Path(stem + ".out").read_text())) + + print( + "| tool | scenario | runs | wall s (median) | wall s (all) | user+sys s (median) " + "| peak RSS MiB (median) | exit codes | findings | parse errors |" + ) + print("|---|---|---|---|---|---|---|---|---|---|") + found = {variant for variant, _ in runs} + variants = [v for v in VARIANT_ORDER if v in found] + sorted(found - set(VARIANT_ORDER)) + for variant in variants: + for scenario in SCENARIOS: + cell = runs.get((variant, scenario)) + if not cell: + continue + walls = [run[0] for run in cell] + counts = sorted(findings[(variant, scenario)]) + print( + f"| {LABELS.get(variant, variant)} | {scenario} | {len(cell)} " + f"| {statistics.median(walls):.2f} " + f"| {', '.join(f'{wall:.2f}' for wall in walls)} " + f"| {statistics.median(run[1] for run in cell):.2f} " + f"| {statistics.median(run[2] for run in cell) / 1024:.0f} " + f"| {','.join(sorted({str(run[3]) for run in cell}))} " + f"| {', '.join(str(count[0]) for count in counts)} " + f"| {', '.join(str(count[1]) for count in counts)} |" + ) + + +if __name__ == "__main__": + main() From b24018317de975683fdef526437b0654ac970f5a Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 19:27:19 +0000 Subject: [PATCH 16/29] chore: keep agent instructions and orb setup out of the repository They move to private configuration. Co-authored-by: Kevin Longe --- .agents/setup | 51 ------------------------------------------------ AGENTS.md | 34 -------------------------------- website/STYLE.md | 2 +- 3 files changed, 1 insertion(+), 86 deletions(-) delete mode 100755 .agents/setup delete mode 100644 AGENTS.md diff --git a/.agents/setup b/.agents/setup deleted file mode 100755 index a6b511e..0000000 --- a/.agents/setup +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env bash -# Prepares a fresh Amp orb: Rust stable, Python 3.12 and the locked uv environment with the -# native sqlclean executable built. Idempotent; a warm run on a restored snapshot only re-verifies. -set -euo pipefail - -repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$repo_root" - -step() { - local name="$1" - shift - local start=$SECONDS - echo "==> ${name}" - "$@" - echo "<== ${name} ($((SECONDS - start))s)" -} - -# One persistent Cargo target directory per worktree, outside the repository. -target_dir="$HOME/.cache/cargo-target/$(basename "$repo_root")" -export CARGO_TARGET_DIR="$target_dir" -export PATH="$HOME/.cargo/bin:$PATH" - -install_rust() { - if ! command -v rustup >/dev/null 2>&1; then - curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ - | sh -s -- -y --no-modify-path --profile minimal --default-toolchain stable \ - --component clippy --component rustfmt - else - rustup update stable --no-self-update - rustup component add clippy rustfmt --toolchain stable - fi -} - -persist_env() { - local marker="# sqlclean orb environment (${repo_root})" - if ! grep -Fqx "$marker" "$HOME/.bash_profile" 2>/dev/null; then - cat >>"$HOME/.bash_profile" < -q -n auto`. -- Run focused Rust tests by package: `cargo test -p `. -- `make check` formats and lints everything; `make verify` is the full CI gate. - -## Public Repository Hygiene - -- Treat every tracked file, fixture, example, commit, branch, pull request, and release as public. -- Use neutral examples and run the public-repository hygiene scanner before the first push. diff --git a/website/STYLE.md b/website/STYLE.md index 480f30e..78709f7 100644 --- a/website/STYLE.md +++ b/website/STYLE.md @@ -36,7 +36,7 @@ These rules apply to every page under `website/src/content/docs/`. Pages that fo - Write short, plain sentences in the present tense. One idea per sentence. - Give every Rule an example where it matters, such as the SQL that triggers a finding and the SQL that fixes it. -- Use neutral orders-style examples (orders, customers, products, payments). See `AGENTS.md`. +- Use neutral orders-style examples (orders, customers, products, payments). ## No history From 64fdb10a7acc346e06ed857efed54642338a517a Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 19:39:08 +0000 Subject: [PATCH 17/29] fix(cli): read a bare --config file name from the working directory `--config custom.toml` made the configuration root an empty path, so every command failed with ": no such file or directory". Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- crates/sqlclean-cli/src/config/_helpers/loading.rs | 2 +- .../e2e/src/sqlclean/hosting/test_sqlclean_command.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/crates/sqlclean-cli/src/config/_helpers/loading.rs b/crates/sqlclean-cli/src/config/_helpers/loading.rs index b3c3100..53d4491 100644 --- a/crates/sqlclean-cli/src/config/_helpers/loading.rs +++ b/crates/sqlclean-cli/src/config/_helpers/loading.rs @@ -12,7 +12,7 @@ use crate::config::models::{Config, RulesConfig}; pub(crate) fn load_config(explicit: Option<&Path>, start: &Path) -> Result { let located: Option = match explicit { - Some(path) => Some(path.to_path_buf()), + Some(path) => Some(start.join(path)), None => find_config(start), }; let (root, file): (PathBuf, ConfigFile) = match located { diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 2cda67d..72ed639 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -68,6 +68,17 @@ expected_codes=(), expected_stale=("z",), ), + CheckTestCase( + description="a bare --config file name is read from the working directory", + config=DUCKDB, + files={ + "strict.toml": CROSS_JOIN_ONLY, + "models/q.sql": "SELECT a.id FROM a CROSS JOIN b\n", + }, + arguments=("--config", "strict.toml"), + expected_exit=1, + expected_codes=("SQC038",), + ), CheckTestCase( description="unparseable SQL is reported as a finding", config=CROSS_JOIN_ONLY, From 966b3214cc336c522d48d01e7c1d1a1ccde5789a Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 19:39:08 +0000 Subject: [PATCH 18/29] perf(custom): evaluate custom Rules on one host per CPU Custom Rules rebuilt each file's facts serially and ran in a single Python process. Facts are now built in parallel, and statement and file Rules split the files across one host per CPU, from 250 files a host. Repository Rules still read every file in one host. On 10,282 files with three Python Rules, check drops from 5.4 s to 2.1 s on 4 CPUs. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .../src/checking/_helpers/custom.rs | 65 +++++++++++++++---- .../sqlclean/hosting/test_sqlclean_command.py | 20 ++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/crates/sqlclean-cli/src/checking/_helpers/custom.rs b/crates/sqlclean-cli/src/checking/_helpers/custom.rs index 22d6a12..e3b881e 100644 --- a/crates/sqlclean-cli/src/checking/_helpers/custom.rs +++ b/crates/sqlclean-cli/src/checking/_helpers/custom.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, BTreeSet}; +use rayon::iter::{IntoParallelIterator, ParallelIterator}; use sha2::{Digest, Sha256}; use sqlclean::sql_facts::main::file_facts::file_facts; @@ -12,10 +13,12 @@ use crate::config::models::Config; use crate::discovery::models::SourceFile; use crate::hosting::constants::EVALUATE_MODE; use crate::hosting::main::run_host::run_host; -use crate::hosting::models::{HostFile, HostFinding, HostRequest}; +use crate::hosting::models::{HostFile, HostFinding, HostRequest, HostResponse, HostResult}; const FILE_RESULT_KEY: &str = "custom-file"; const REPOSITORY_RESULT_KEY: &str = "custom-repository"; +/// The fewest files worth starting another host process for. +const MIN_FILES_PER_HOST: usize = 250; /// What custom evaluation needs besides the files. pub(crate) struct CustomEvaluation<'a> { @@ -37,7 +40,7 @@ pub(crate) fn custom_findings( .partition(|rule| rule.subject != RuleSubject::Repository); let mut findings: Vec = Vec::new(); let mut misses: Vec<(String, Option, String)> = Vec::new(); - let mut host_files: Vec = Vec::new(); + let mut pending: Vec<(&SourceFile, Vec)> = Vec::new(); for file in files { let mut missing: Vec = Vec::new(); for rule in &file_rules { @@ -53,7 +56,7 @@ pub(crate) fn custom_findings( } } if !missing.is_empty() { - host_files.push(host_file(file, missing)?); + pending.push((file, missing)); } } let repository_digest: String = repository_digest(files); @@ -77,19 +80,17 @@ pub(crate) fn custom_findings( if misses.is_empty() { return Ok(findings); } + let mut host_files: Vec = pending + .into_par_iter() + .map(|(file, missing)| host_file(file, missing)) + .collect::>()?; if !missing_repository.is_empty() { host_files = with_every_file(files, host_files)?; } - let response = run_host(&HostRequest { - mode: EVALUATE_MODE.to_owned(), - root: evaluation.config.root.to_string_lossy().into_owned(), - custom: evaluation.config.rules.custom.clone(), - options, - files: host_files, - repository_rules: missing_repository, - })?; + let results: Vec = + evaluate_on_hosts(evaluation, options, host_files, missing_repository)?; let mut by_subject: BTreeMap<(Option, String), Vec> = BTreeMap::new(); - for result in response.results { + for result in results { by_subject.insert((result.path, result.code), result.findings); } for (key, path, code) in misses { @@ -104,6 +105,46 @@ pub(crate) fn custom_findings( Ok(findings) } +/// Evaluate on one host per share of the files; repository Rules need every file in one host. +fn evaluate_on_hosts( + evaluation: &CustomEvaluation<'_>, + options: BTreeMap, + host_files: Vec, + repository_rules: Vec, +) -> Result, String> { + let request = |files: Vec, repository_rules: Vec| HostRequest { + mode: EVALUATE_MODE.to_owned(), + root: evaluation.config.root.to_string_lossy().into_owned(), + custom: evaluation.config.rules.custom.clone(), + options: options.clone(), + files, + repository_rules, + }; + let hosts: usize = + (host_files.len() / MIN_FILES_PER_HOST).clamp(1, rayon::current_num_threads()); + if hosts == 1 || !repository_rules.is_empty() { + return Ok(run_host(&request(host_files, repository_rules))?.results); + } + let share: usize = host_files.len().div_ceil(hosts); + let mut shares: Vec> = Vec::with_capacity(hosts); + let mut rest: Vec = host_files; + while rest.len() > share { + let tail: Vec = rest.split_off(share); + shares.push(rest); + rest = tail; + } + shares.push(rest); + let responses: Vec> = shares + .into_par_iter() + .map(|files| run_host(&request(files, Vec::new()))) + .collect(); + let mut results: Vec = Vec::new(); + for response in responses { + results.extend(response?.results); + } + Ok(results) +} + fn rule_options( evaluation: &CustomEvaluation<'_>, ) -> Result, String> { diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 72ed639..309add9 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -339,3 +339,23 @@ def test_given_unchanged_files_when_checking_again_then_custom_rules_come_from_t assert (first.returncode, cached.returncode, changed.returncode) == test_case.expected_exits assert test_case.expected_cached_code in cached.stdout assert test_case.expected_error in changed.stderr + + +def test_given_many_files_when_checking_custom_rules_then_every_host_share_reports( + tmp_path: Path, +) -> None: + files: dict[str, str] = { + f"models/m{index:04}.sql": f"SELECT a FROM {'tmp_x' if index % 3 == 0 else 'x'}\n" + for index in range(1200) + } + write_project(tmp_path, config=CUSTOM, files={"sql_rules/naming.py": SAMPLE_RULES, **files}) + + completed: subprocess.CompletedProcess[str] = run( + tmp_path, "check", "--output-format", "json", "--select", "XSQCNAME001", environment={} + ) + report: dict[str, list[dict[str, object]]] = json.loads(completed.stdout) + + assert completed.returncode == 1, completed.stderr + assert [finding["path"] for finding in report["findings"]] == [ + f"models/m{index:04}.sql" for index in range(0, 1200, 3) + ] From 933e62e404b600c84d32e1a3ef57af238067f422 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 19:46:05 +0000 Subject: [PATCH 19/29] chore(bench): add a custom Rules variant sqlclean_custom runs the default Rules plus three Python Rules that a data team might write: layering between marts, staging and raw sources, and a ban on nondeterministic functions. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- benchmarks/linters/README.md | 1 + benchmarks/linters/bench.sh | 13 +++++-- benchmarks/linters/rules/layers.py | 61 ++++++++++++++++++++++++++++++ benchmarks/linters/summarize.py | 2 + 4 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 benchmarks/linters/rules/layers.py diff --git a/benchmarks/linters/README.md b/benchmarks/linters/README.md index a3751b5..e91e37c 100644 --- a/benchmarks/linters/README.md +++ b/benchmarks/linters/README.md @@ -35,6 +35,7 @@ and peak memory with `/usr/bin/time`, and every lint command writes JSON finding |---|---| | `sqlclean` | `sqlclean check --output-format json` (13 default Rules) | | `sqlclean_all` | `sqlclean check --select SQC --output-format json` (all 45 Rules) | +| `sqlclean_custom` | `sqlclean check --config custom.toml --output-format json` (default Rules and the three Python Rules in `rules/`) | | `sqlclean_format` | `sqlclean format --check` | | `sqruff` | `sqruff lint -f json .` | | `sqlfluff` | `sqlfluff lint -f json --processes $(nproc) --disable-progress-bar .` | diff --git a/benchmarks/linters/bench.sh b/benchmarks/linters/bench.sh index a0bc690..643b744 100755 --- a/benchmarks/linters/bench.sh +++ b/benchmarks/linters/bench.sh @@ -18,12 +18,13 @@ # SQLFLUFF sqlfluff executable (default: sqlfluff) # PROCESSES SQLFluff worker processes (default: nproc). SQLClean and sqruff pick their own. # -# Default variants: sqlclean sqlclean_all sqlclean_format sqruff sqlfluff. +# Default variants: sqlclean sqlclean_all sqlclean_custom sqlclean_format sqruff sqlfluff. set -u CORPUS=$(realpath "$1") OUT=$(realpath -m "$2") ROUNDS=$3 shift 3 +HERE=$(cd "$(dirname "$0")" && pwd) SQLCLEAN=${SQLCLEAN:-sqlclean} SQRUFF=${SQRUFF:-sqruff} SQLFLUFF=${SQLFLUFF:-sqlfluff} @@ -34,16 +35,18 @@ trap 'rm -rf "$WORK"' EXIT declare -A CMD CMD[sqlclean]="$SQLCLEAN check --output-format json" CMD[sqlclean_all]="$SQLCLEAN check --select SQC --output-format json" +# The default Rules plus the three Python Rules in rules/, copied into the work directory. +CMD[sqlclean_custom]="$SQLCLEAN check --config custom.toml --output-format json" # sqruff's and SQLFluff's default rules include layout; SQLClean checks layout with its formatter. CMD[sqlclean_format]="$SQLCLEAN format --check" CMD[sqruff]="$SQRUFF lint -f json ." CMD[sqlfluff]="$SQLFLUFF lint -f json --processes $PROCESSES --disable-progress-bar ." # SQLClean reuses results for unchanged files; the others have no result cache. -WARM_VARIANTS=" sqlclean sqlclean_all " +WARM_VARIANTS=" sqlclean sqlclean_all sqlclean_custom " VARIANTS=("$@") if [ ${#VARIANTS[@]} -eq 0 ]; then - VARIANTS=(sqlclean sqlclean_all sqlclean_format sqruff sqlfluff) + VARIANTS=(sqlclean sqlclean_all sqlclean_custom sqlclean_format sqruff sqlfluff) fi for v in "${VARIANTS[@]}"; do if [ -z "${CMD[$v]+set}" ]; then @@ -83,6 +86,10 @@ for round in $(seq 1 "$ROUNDS"); do if [ $((round % 2)) -eq 0 ]; then v=${VARIANTS[$((n - 1 - i))]}; else v=${VARIANTS[$i]}; fi rm -rf "${WORK:?}/$v" cp -a "$CORPUS" "$WORK/$v" + if [ "$v" = sqlclean_custom ]; then + cp -r "$HERE/rules" "$WORK/$v/bench_rules" + printf 'dialect = "snowflake"\n[rules]\ncustom = ["bench_rules"]\n' >"$WORK/$v/custom.toml" + fi sync measure "$v" cold "$round" [[ $WARM_VARIANTS == *" $v "* ]] && measure "$v" warm "$round" diff --git a/benchmarks/linters/rules/layers.py b/benchmarks/linters/rules/layers.py new file mode 100644 index 0000000..f3b25af --- /dev/null +++ b/benchmarks/linters/rules/layers.py @@ -0,0 +1,61 @@ +"""Benchmark custom Rules: layering and determinism checks a data team might write.""" + +from sqlclean import Finding, RuleContext, Statement, rule + +NONDETERMINISTIC_FUNCTIONS: frozenset[str] = frozenset( + {"CURRENT_DATE", "CURRENT_TIMESTAMP", "GETDATE", "NOW", "RANDOM", "SYSDATE", "UUID_STRING"} +) + + +@rule( + code="XSQCLAYER001", + message="Mart reads a raw source", + remediation="Read the staging model instead.", + enabled_by_default=True, +) +def marts_read_staging(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + """Flag every raw table a mart reads.""" + + if not statement.path.startswith("marts/"): + return [] + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.qualified_name.startswith("raw.") + ] + + +@rule( + code="XSQCLAYER002", + message="Staging model reads another model", + remediation="Staging models read only raw sources and seeds.", + enabled_by_default=True, +) +def staging_reads_sources(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + """Flag every table a staging model reads that is neither raw, a seed nor one of its CTEs.""" + + if not statement.path.startswith("staging/"): + return [] + ctes: set[str] = {cte.name.lower() for cte in statement.ctes} + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if not table.qualified_name.startswith(("raw.", "seeds.")) + and table.name.lower() not in ctes + ] + + +@rule( + code="XSQCDET001", + message="Model calls a nondeterministic function", + remediation="Pass the time or seed in as a column so reruns give the same rows.", + enabled_by_default=True, +) +def deterministic_functions(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + """Flag calls such as CURRENT_TIMESTAMP that change between runs.""" + + return [ + ctx.finding(subject=statement, node=function) + for function in statement.functions + if function.name.upper() in NONDETERMINISTIC_FUNCTIONS + ] diff --git a/benchmarks/linters/summarize.py b/benchmarks/linters/summarize.py index c77d8b1..f58f32c 100644 --- a/benchmarks/linters/summarize.py +++ b/benchmarks/linters/summarize.py @@ -14,6 +14,7 @@ VARIANT_ORDER: tuple[str, ...] = ( "sqlclean", "sqlclean_all", + "sqlclean_custom", "sqlclean_format", "sqruff", "sqlfluff", @@ -21,6 +22,7 @@ LABELS: dict[str, str] = { "sqlclean": "SQLClean, default Rules", "sqlclean_all": "SQLClean, every built-in Rule", + "sqlclean_custom": "SQLClean, default Rules and three Python Rules", "sqlclean_format": "SQLClean formatter, `format --check`", "sqruff": "sqruff, default rules", "sqlfluff": "SQLFluff, default rules", From b1b5182b166b5f79cb44956016e15ec75ec25350 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 19:46:05 +0000 Subject: [PATCH 20/29] docs(website): lead with the benchmark and custom Rules - The hero shows a real-time race on the 10,282-file benchmark: the sqlclean bar fills in 0.81 s while sqruff and SQLFluff advance at their measured rates. Reduced motion shows the moment sqlclean ends. - Three feature boxes replace six: speed, custom Rules in Python and safe fixes. - The Benchmarks page adds the custom Rules row, and the custom Rules docs say statement and file Rules can run in several processes. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- website/public/index.html | 98 +++++++++++-------- website/public/styles.css | 77 +++++++++++++++ website/src/content/docs/docs/benchmarks.mdx | 13 +++ .../docs/docs/custom-rules/overview.mdx | 3 + 4 files changed, 152 insertions(+), 39 deletions(-) diff --git a/website/public/index.html b/website/public/index.html index 5b4ef9c..803ec02 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -56,11 +56,11 @@

    Rust-powered · Free and open source · Apache-2.0

    Clean SQL, every time

    -

    A fast SQL linter and formatter.

    +

    A SQL linter and formatter, 50× faster than sqruff.

    SQLClean flags SQL that runs but is easy to get wrong, like = NULL, applies safe fixes, and - formats every file one way. It checks 10,000 files in under a second. Your team's own Rules are a few - lines of Python. + formats every file one way. Your team's own Rules are plain Python functions, and 10,000 files still + check in about two seconds.

    @@ -72,21 +72,28 @@

    Clean SQL, every time

    + aria-label="Linting the same 10,282 SQL files on 4 CPUs: sqlclean check finishes in 0.81 seconds, sqruff takes 41 seconds and SQLFluff 919 seconds." + data-race + > + -- 10,282 Snowflake files · 4 CPUs · real time + sqlclean check0.81 s + sqruff lint41 s + sqlfluff lint15 min + elapsed 0.0 s · See the benchmark +
    @@ -235,8 +242,9 @@

    Stop arguing about layout

    04 · Make lessons permanent

    Turn review comments into Rules

    - A custom Rule is a Python function that reads the tables, joins, columns and literals of each statement, - already located for you. Here marts must read staging models, never raw tables. + A custom Rule is a Python function over the tables, joins, columns and literals of each statement, + already parsed and located for you. No regular expressions, no parser to learn. Here marts must read + staging models, never raw tables.

    Write a custom Rule →

    @@ -362,33 +370,21 @@

    Before every commit and in CI

    -

    Built for big SQL repositories

    -

    The rest of what makes SQLClean easy to adopt.

    +

    Fast, and still yours to extend

    +

    Measured on the same 10,282 SQL files. See the benchmark →

    -

    13 Rules on by default, 45 in all

    -

    The defaults cover NULLs, joins, ordering and duplicates. The other 32 cover style and structure, and you opt in to them.

    -
    -
    -

    Faster than sqruff and SQLFluff

    -

    On 10,282 files, sqlclean check takes 0.8 s with defaults; sqruff takes 41 s and SQLFluff 15 minutes. See the benchmark →

    +

    50× faster than sqruff

    +

    sqlclean check takes 0.8 s, sqruff 41 s and SQLFluff 15 minutes. Unchanged files come from the cache, so a rerun takes 0.2 s.

    -

    A dialect per folder

    -

    Set one dialect for the project and override it for any folder, such as Snowflake models beside DuckDB tests.

    +

    Custom Rules in plain Python

    +

    A Rule reads the tables, joins and columns of each query, not its text. With three custom Rules, the same files check in 2.3 s.

    -

    Exact locations

    -

    Findings and fixes use character offsets, so multibyte text doesn't shift an edit.

    -
    -
    -

    Text, JSON and SARIF

    -

    Readable in a terminal, structured for scripts, and annotated in code review.

    -
    -
    -

    An embeddable Rust crate

    -

    The Rules and formatter live in their own crate, so another tool can run them under its own codes.

    +

    Safe fixes by default

    +

    A fix that could change which rows a query returns waits for --unsafe-fixes. A fix that breaks parsing is thrown away.

    @@ -459,6 +455,30 @@

    Make clean SQL the default

    document.documentElement.dataset.theme = next; localStorage.setItem('sqlclean-theme', next); }); + (function () { + var race = document.querySelector('[data-race]'); + if (!race || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + var rows = Array.prototype.map.call(race.querySelectorAll('.race-row'), function (row) { + return { seconds: Number(row.dataset.seconds), fill: row.querySelector('.race-fill'), time: row.querySelector('.race-time') }; + }); + var clock = race.querySelector('.race-clock'); + var lead = 0.6, run = 8, hold = 2.4, start = null; + race.classList.add('is-live'); + function frame(now) { + if (start === null) start = now; + var elapsed = Math.min(Math.max((now - start) / 1000 - lead, 0), run); + rows.forEach(function (row) { + var done = elapsed >= row.seconds; + row.fill.style.transform = 'scaleX(' + Math.min(elapsed / row.seconds, 1) + ')'; + row.time.textContent = done ? row.seconds.toFixed(2) + ' s ✓' : (100 * elapsed / row.seconds).toFixed(1) + '%'; + row.time.classList.toggle('ok', done); + }); + clock.textContent = 'elapsed ' + elapsed.toFixed(1) + ' s · '; + if ((now - start) / 1000 > lead + run + hold) start = now; + requestAnimationFrame(frame); + } + requestAnimationFrame(frame); + })(); document.querySelectorAll('[data-copy]').forEach(function (button) { button.addEventListener('click', function () { navigator.clipboard.writeText(button.dataset.copy).then(function () { diff --git a/website/public/styles.css b/website/public/styles.css index e8a210e..26ac22f 100644 --- a/website/public/styles.css +++ b/website/public/styles.css @@ -893,6 +893,83 @@ footer a { animation-name: hv-l2; } +/* Benchmark race: real elapsed time; the script drives it, the markup shows final results. */ +.race { + display: grid; + align-content: center; + gap: 14px; + box-sizing: border-box; + width: 520px; + max-width: 100%; + font-size: 13px; + white-space: normal; +} + + +.race-row { + display: grid; + grid-template-columns: 9.5em minmax(0, 1fr) 6.5em; + align-items: center; + gap: 12px; +} + +.race-row .cmd { + white-space: nowrap; +} + +.race-bar { + height: 10px; + border-radius: var(--radius); + background: var(--inner-line); + overflow: hidden; +} + +.race-fill { + display: block; + height: 100%; + background: var(--faint); + transform-origin: left; +} + +.race .is-sqlclean .race-fill { + background: var(--primary); +} + +.race-time { + text-align: right; + font-variant-numeric: tabular-nums; +} + +.race .is-sqlclean .race-time { + color: var(--primary); + font-weight: 500; +} + +.race-foot { + font-variant-numeric: tabular-nums; +} + +.race-foot a { + color: inherit; +} + +.race:not(.is-live) .race-clock { + display: none; +} + +@media (max-width: 620px) { + .race { + width: 100%; + min-height: 0; + font-size: 11px; + } + + .race-row { + grid-template-columns: 10em minmax(0, 1fr) 5em; + gap: 8px; + } +} + @media (prefers-reduced-motion: reduce) { .hero-visual .hv-a, .hero-visual .hv-b, diff --git a/website/src/content/docs/docs/benchmarks.mdx b/website/src/content/docs/docs/benchmarks.mdx index c3b8427..d75f38e 100644 --- a/website/src/content/docs/docs/benchmarks.mdx +++ b/website/src/content/docs/docs/benchmarks.mdx @@ -12,6 +12,7 @@ SQLFluff ran once. |---|---:|---:|---:|---:| | `sqlclean check` (13 default Rules) | **0.81 s** | 2.5 s | 48 MiB | 1× | | `sqlclean check --select SQC` (all 45 Rules) | **1.33 s** | 3.8 s | 140 MiB | 1.6× | +| `sqlclean check` with three [custom Rules](#custom-rules) | **2.27 s** | 6.9 s | 182 MiB | 2.8× | | `sqlclean format --check` | **3.03 s** | 11.5 s | 49 MiB | 3.7× | | `sqruff lint` | 41.0 s | 114 s | 235 MiB | 51× | | `sqlfluff lint --processes 4` | 919 s | 2,796 s | 992 MiB | 1,135× | @@ -19,6 +20,18 @@ SQLFluff ran once. Run again with nothing changed, `sqlclean check` reads its result cache and finishes in 0.23 s. sqruff and SQLFluff have no result cache, so a second run costs the same as the first. +## Custom Rules + +The custom variant adds three Python Rules from `benchmarks/linters/rules/` to the default Rules: +marts must not read raw sources, staging models read only sources and seeds, and no model calls a +nondeterministic function such as `CURRENT_TIMESTAMP`. They report 3,355 findings on top of the +default Rules' 3,872. + +SQLClean builds each statement's facts in Rust and evaluates statement and file Rules in one Python +process per CPU, so custom Rules use every core. A Rule over the whole repository runs in one +process, because it reads every file. Run again with nothing changed, the custom variant reads its +result cache and finishes in 0.46 s. + ## Comparing like with like The tools don't check the same things. Almost every sqruff and SQLFluff finding on this corpus diff --git a/website/src/content/docs/docs/custom-rules/overview.mdx b/website/src/content/docs/docs/custom-rules/overview.mdx index e96880b..bc04aef 100644 --- a/website/src/content/docs/docs/custom-rules/overview.mdx +++ b/website/src/content/docs/docs/custom-rules/overview.mdx @@ -139,6 +139,9 @@ A Repository Rule passes the statement or file each finding belongs to as `subje - Custom Rules run after the built-in Rules, on files that parse. A file with a [parse error](/docs/rules/overview/#parse-errors-sqc999) gets no custom Rule findings. +- From 500 files, Statement and File Rules split the files across one Python process per CPU. A + Rule can't share state between files, so anything that spans files belongs in a Repository Rule, + which always runs in one process with every file. - The working directory is the configuration root, and `PYTHONHASHSEED` is `0`, so iterating a set of strings gives the same order on every run. - Results are cached with the built-in results. Editing any Python file under `[rules] custom` From aadba241c148195545971f4a3ac5f75158ff0cc6 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 20:10:03 +0000 Subject: [PATCH 21/29] docs(website): show race totals in time and say custom Rules run on the Rust engine - The race labels each linter with its total time instead of a percentage. - The lede describes the NULL example in words. - The custom Rules box says the Rust engine parses SQL and builds the facts, so Python only runs the Rule's logic. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- website/public/index.html | 14 +++++++------- website/public/styles.css | 4 ++++ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/website/public/index.html b/website/public/index.html index 803ec02..20537a9 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -58,9 +58,9 @@

    Clean SQL, every time

    A SQL linter and formatter, 50× faster than sqruff.

    - SQLClean flags SQL that runs but is easy to get wrong, like = NULL, applies safe fixes, and - formats every file one way. Your team's own Rules are plain Python functions, and 10,000 files still - check in about two seconds. + SQLClean flags SQL that runs without an error but rarely does what you meant, such as a comparison with + NULL that never matches. It applies safe fixes and formats every file one way. Your team's own Rules are + plain Python functions, and 10,000 files still check in about two seconds.

    @@ -380,7 +380,7 @@

    50× faster than sqruff

    Custom Rules in plain Python

    -

    A Rule reads the tables, joins and columns of each query, not its text. With three custom Rules, the same files check in 2.3 s.

    +

    A Rule is a Python function over the tables, joins and columns of each query. The Rust engine parses the SQL and builds those facts, so Python only runs your logic: three custom Rules check the same files in 2.3 s.

    Safe fixes by default

    @@ -459,7 +459,7 @@

    Make clean SQL the default

    var race = document.querySelector('[data-race]'); if (!race || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; var rows = Array.prototype.map.call(race.querySelectorAll('.race-row'), function (row) { - return { seconds: Number(row.dataset.seconds), fill: row.querySelector('.race-fill'), time: row.querySelector('.race-time') }; + return { seconds: Number(row.dataset.seconds), fill: row.querySelector('.race-fill'), time: row.querySelector('.race-time'), total: row.querySelector('.race-time').textContent }; }); var clock = race.querySelector('.race-clock'); var lead = 0.6, run = 8, hold = 2.4, start = null; @@ -470,8 +470,8 @@

    Make clean SQL the default

    rows.forEach(function (row) { var done = elapsed >= row.seconds; row.fill.style.transform = 'scaleX(' + Math.min(elapsed / row.seconds, 1) + ')'; - row.time.textContent = done ? row.seconds.toFixed(2) + ' s ✓' : (100 * elapsed / row.seconds).toFixed(1) + '%'; - row.time.classList.toggle('ok', done); + row.time.textContent = done ? row.total + ' ✓' : row.total; + row.time.classList.toggle('dim', !done); }); clock.textContent = 'elapsed ' + elapsed.toFixed(1) + ' s · '; if ((now - start) / 1000 > lead + run + hold) start = now; diff --git a/website/public/styles.css b/website/public/styles.css index 26ac22f..28cd94a 100644 --- a/website/public/styles.css +++ b/website/public/styles.css @@ -945,6 +945,10 @@ footer a { font-weight: 500; } +.race .race-row .race-time.dim { + color: var(--faint); +} + .race-foot { font-variant-numeric: tabular-nums; } From 6930cf22662c4bfbaa247b13bdc8f64489a20e70 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 20:15:49 +0000 Subject: [PATCH 22/29] test(e2e): parametrize the many-files custom Rules test fensu requires dataclass cases with an expected_ field and no conditionals in tests. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- tests/e2e/src/sqlclean/hosting/_test_types.py | 10 ++++++++ .../sqlclean/hosting/test_sqlclean_command.py | 23 +++++++++++++++---- 2 files changed, 29 insertions(+), 4 deletions(-) diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py index aa1face..109abb6 100644 --- a/tests/e2e/src/sqlclean/hosting/_test_types.py +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -69,3 +69,13 @@ class SarifTestCase: config: str source: str expected_message: str + + +@dataclass(frozen=True) +class ManyFilesTestCase: + """Enough files for custom Rules to run on several hosts, every flagged_every-th one flagged.""" + + description: str + file_count: int + flagged_every: int + expected_finding_count: int diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 309add9..4520e09 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -12,6 +12,7 @@ CacheTestCase, CheckTestCase, IdempotencyTestCase, + ManyFilesTestCase, RewriteTestCase, SarifTestCase, SummaryTestCase, @@ -341,12 +342,25 @@ def test_given_unchanged_files_when_checking_again_then_custom_rules_come_from_t assert test_case.expected_error in changed.stderr +@pytest.mark.parametrize( + "test_case", + [ + ManyFilesTestCase( + description="1,200 files split across hosts report every finding in file order", + file_count=1200, + flagged_every=3, + expected_finding_count=400, + ), + ], + ids=lambda case: case.description, +) def test_given_many_files_when_checking_custom_rules_then_every_host_share_reports( - tmp_path: Path, + tmp_path: Path, test_case: ManyFilesTestCase ) -> None: + flagged: range = range(0, test_case.file_count, test_case.flagged_every) files: dict[str, str] = { - f"models/m{index:04}.sql": f"SELECT a FROM {'tmp_x' if index % 3 == 0 else 'x'}\n" - for index in range(1200) + **{f"models/m{index:04}.sql": "SELECT a FROM x\n" for index in range(test_case.file_count)}, + **{f"models/m{index:04}.sql": "SELECT a FROM tmp_x\n" for index in flagged}, } write_project(tmp_path, config=CUSTOM, files={"sql_rules/naming.py": SAMPLE_RULES, **files}) @@ -356,6 +370,7 @@ def test_given_many_files_when_checking_custom_rules_then_every_host_share_repor report: dict[str, list[dict[str, object]]] = json.loads(completed.stdout) assert completed.returncode == 1, completed.stderr + assert len(report["findings"]) == test_case.expected_finding_count assert [finding["path"] for finding in report["findings"]] == [ - f"models/m{index:04}.sql" for index in range(0, 1200, 3) + f"models/m{index:04}.sql" for index in flagged ] From 0fdca9a9e3437d2b3427a09a4b92de4acb4c974d Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 20:15:50 +0000 Subject: [PATCH 23/29] feat(cli): accept lint as an alias of check SQLFluff and sqruff users type lint. The alias is hidden from --help; check stays the documented name, and the check page mentions lint. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- crates/sqlclean-cli/src/command/models.rs | 1 + tests/e2e/src/sqlclean/hosting/_test_types.py | 1 + .../e2e/src/sqlclean/hosting/test_sqlclean_command.py | 11 ++++++++++- website/src/content/docs/docs/cli/check.mdx | 2 ++ 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/crates/sqlclean-cli/src/command/models.rs b/crates/sqlclean-cli/src/command/models.rs index 1b9df67..0d04fc5 100644 --- a/crates/sqlclean-cli/src/command/models.rs +++ b/crates/sqlclean-cli/src/command/models.rs @@ -20,6 +20,7 @@ pub struct Cli { #[derive(Debug, Subcommand)] pub enum Command { /// Report Rule findings without changing any file. + #[command(alias = "lint")] Check(CheckArgs), /// Apply Rule fixes until stable, then format. Fix(FixArgs), diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py index 109abb6..9e0aab7 100644 --- a/tests/e2e/src/sqlclean/hosting/_test_types.py +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -16,6 +16,7 @@ class CheckTestCase: expected_exit: int expected_codes: tuple[str, ...] expected_stale: tuple[str, ...] = () + command: str = "check" @dataclass(frozen=True) diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 4520e09..289c497 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -80,6 +80,15 @@ expected_exit=1, expected_codes=("SQC038",), ), + CheckTestCase( + description="lint is check by another name", + config=CROSS_JOIN_ONLY, + files={"models/q.sql": "SELECT a.id FROM a CROSS JOIN b\n"}, + arguments=(), + command="lint", + expected_exit=1, + expected_codes=("SQC038",), + ), CheckTestCase( description="unparseable SQL is reported as a finding", config=CROSS_JOIN_ONLY, @@ -108,7 +117,7 @@ def test_given_project_when_checking_then_reports_findings_and_exit_status( write_project(tmp_path, config=test_case.config, files=test_case.files) completed: subprocess.CompletedProcess[str] = run( - tmp_path, "check", "--output-format", "json", *test_case.arguments, environment={} + tmp_path, test_case.command, "--output-format", "json", *test_case.arguments, environment={} ) report: dict[str, list[dict[str, object]]] = json.loads(completed.stdout) diff --git a/website/src/content/docs/docs/cli/check.mdx b/website/src/content/docs/docs/cli/check.mdx index bbfc420..a288612 100644 --- a/website/src/content/docs/docs/cli/check.mdx +++ b/website/src/content/docs/docs/cli/check.mdx @@ -8,6 +8,8 @@ sidebar: `sqlclean check` runs the selected Rules over SQL files and reports every finding. It never changes a file. +`sqlclean lint` is the same command, for anyone used to SQLFluff or sqruff. + ## Usage ```bash From a674bd2c3790b8ee0d506c78ff1aa0b7ba6544a9 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 20:17:36 +0000 Subject: [PATCH 24/29] docs(website): shorten the hero lede Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- website/public/index.html | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/website/public/index.html b/website/public/index.html index 20537a9..0b6ff2f 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -58,9 +58,8 @@

    Clean SQL, every time

    A SQL linter and formatter, 50× faster than sqruff.

    - SQLClean flags SQL that runs without an error but rarely does what you meant, such as a comparison with - NULL that never matches. It applies safe fixes and formats every file one way. Your team's own Rules are - plain Python functions, and 10,000 files still check in about two seconds. + SQLClean flags SQL that runs but rarely does what you meant, applies safe fixes, and formats every file + one way. Custom Rules are plain Python functions, backed by a Rust API that keeps them fast.

    From 49406354199082726e11c0ae010882064c7c2198 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 21:10:45 +0000 Subject: [PATCH 25/29] docs(website): serve the site from sqlclean.io Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- website/astro.config.mjs | 6 +++--- website/public/index.html | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/website/astro.config.mjs b/website/astro.config.mjs index 2972438..a4f4b43 100644 --- a/website/astro.config.mjs +++ b/website/astro.config.mjs @@ -5,7 +5,7 @@ import { sqlcleanDark, sqlcleanLight } from './src/code-themes.mjs'; import { redirects, sidebar } from './src/navigation.mjs'; export default defineConfig({ - site: 'https://sqlclean.com', + site: 'https://sqlclean.io', redirects, integrations: [ starlight({ @@ -18,10 +18,10 @@ export default defineConfig({ }, favicon: '/favicon.png', head: [ - { tag: 'meta', attrs: { property: 'og:image', content: 'https://sqlclean.com/assets/og-sqlclean-docs.png' } }, + { tag: 'meta', attrs: { property: 'og:image', content: 'https://sqlclean.io/assets/og-sqlclean-docs.png' } }, { tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } }, { tag: 'meta', attrs: { property: 'og:image:height', content: '630' } }, - { tag: 'meta', attrs: { name: 'twitter:image', content: 'https://sqlclean.com/assets/og-sqlclean-docs.png' } }, + { tag: 'meta', attrs: { name: 'twitter:image', content: 'https://sqlclean.io/assets/og-sqlclean-docs.png' } }, ], social: [ { icon: 'github', label: 'GitHub', href: 'https://github.com/sqlstack-io/sqlclean' }, diff --git a/website/public/index.html b/website/public/index.html index 0b6ff2f..6e5c495 100644 --- a/website/public/index.html +++ b/website/public/index.html @@ -11,13 +11,13 @@ - - + + - + From 5b1ccbf4ad85fc2b8edb9ccee826f74b7a6bfe26 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 21:14:44 +0000 Subject: [PATCH 26/29] chore: trigger the first site preview build Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe From 6c04ba8956a1bff40fbe87bae75901ac8efbb1f3 Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 21:29:10 +0000 Subject: [PATCH 27/29] feat(cli): add sqlclean skills to install a bundled agent skill The executable embeds a SQLClean skill with a copy of every docs page. make skills regenerates the copies from the website docs and make check-docs-skill fails while they differ. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- Cargo.lock | 20 ++ Cargo.toml | 1 + Makefile | 9 +- README.md | 1 + crates/sqlclean-cli/Cargo.toml | 1 + crates/sqlclean-cli/build.rs | 5 + crates/sqlclean-cli/skill/SKILL.md | 61 ++++ .../skill/references/docs/CONTENTS.md | 39 +++ .../skill/references/docs/benchmarks.md | 80 +++++ .../skill/references/docs/cli/check.md | 170 +++++++++++ .../skill/references/docs/cli/facts.md | 78 +++++ .../skill/references/docs/cli/fix.md | 99 +++++++ .../skill/references/docs/cli/format.md | 77 +++++ .../skill/references/docs/cli/rules.md | 122 ++++++++ .../skill/references/docs/cli/skills.md | 61 ++++ .../skill/references/docs/configuration.md | 188 ++++++++++++ .../docs/custom-rules/fixes-and-options.md | 160 ++++++++++ .../references/docs/custom-rules/overview.md | 194 ++++++++++++ .../docs/custom-rules/statement-facts.md | 186 ++++++++++++ .../references/docs/custom-rules/testing.md | 183 ++++++++++++ .../skill/references/docs/embedding.md | 160 ++++++++++ .../skill/references/docs/formatting.md | 200 +++++++++++++ .../skill/references/docs/index.md | 83 ++++++ .../skill/references/docs/integrations.md | 116 ++++++++ .../skill/references/docs/quickstart.md | 223 ++++++++++++++ .../skill/references/docs/rules/overview.md | 273 +++++++++++++++++ .../skill/references/docs/rules/reference.md | 275 ++++++++++++++++++ .../src/command/_helpers/dispatch.rs | 3 + .../sqlclean-cli/src/command/_helpers/mod.rs | 1 + .../src/command/_helpers/skills_command.rs | 140 +++++++++ crates/sqlclean-cli/src/command/constants.rs | 15 + crates/sqlclean-cli/src/command/models.rs | 15 + fensu.toml | 6 +- packages/sqlclean-cli/pyproject.toml | 1 + pyproject.toml | 2 + scripts/__init__.py | 0 scripts/check_docs_skill.py | 15 + scripts/docs_skill/__init__.py | 0 scripts/docs_skill/_helpers/__init__.py | 0 scripts/docs_skill/_helpers/mdx.py | 85 ++++++ scripts/docs_skill/_helpers/navigation.py | 65 +++++ scripts/docs_skill/_helpers/output.py | 47 +++ scripts/docs_skill/_helpers/render.py | 110 +++++++ scripts/docs_skill/constants.py | 18 ++ scripts/docs_skill/main/__init__.py | 0 scripts/docs_skill/main/check.py | 41 +++ scripts/docs_skill/main/generate.py | 30 ++ scripts/docs_skill/models.py | 21 ++ scripts/generate_docs_skill.py | 15 + tests/e2e/src/sqlclean/hosting/_test_types.py | 11 + .../sqlclean/hosting/test_sqlclean_command.py | 66 +++++ website/src/content/docs/docs/cli/skills.mdx | 60 ++++ website/src/sidebar.json | 3 +- 53 files changed, 3832 insertions(+), 3 deletions(-) create mode 100644 crates/sqlclean-cli/build.rs create mode 100644 crates/sqlclean-cli/skill/SKILL.md create mode 100644 crates/sqlclean-cli/skill/references/docs/CONTENTS.md create mode 100644 crates/sqlclean-cli/skill/references/docs/benchmarks.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/check.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/facts.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/fix.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/format.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/rules.md create mode 100644 crates/sqlclean-cli/skill/references/docs/cli/skills.md create mode 100644 crates/sqlclean-cli/skill/references/docs/configuration.md create mode 100644 crates/sqlclean-cli/skill/references/docs/custom-rules/fixes-and-options.md create mode 100644 crates/sqlclean-cli/skill/references/docs/custom-rules/overview.md create mode 100644 crates/sqlclean-cli/skill/references/docs/custom-rules/statement-facts.md create mode 100644 crates/sqlclean-cli/skill/references/docs/custom-rules/testing.md create mode 100644 crates/sqlclean-cli/skill/references/docs/embedding.md create mode 100644 crates/sqlclean-cli/skill/references/docs/formatting.md create mode 100644 crates/sqlclean-cli/skill/references/docs/index.md create mode 100644 crates/sqlclean-cli/skill/references/docs/integrations.md create mode 100644 crates/sqlclean-cli/skill/references/docs/quickstart.md create mode 100644 crates/sqlclean-cli/skill/references/docs/rules/overview.md create mode 100644 crates/sqlclean-cli/skill/references/docs/rules/reference.md create mode 100644 crates/sqlclean-cli/src/command/_helpers/skills_command.rs create mode 100644 scripts/__init__.py create mode 100644 scripts/check_docs_skill.py create mode 100644 scripts/docs_skill/__init__.py create mode 100644 scripts/docs_skill/_helpers/__init__.py create mode 100644 scripts/docs_skill/_helpers/mdx.py create mode 100644 scripts/docs_skill/_helpers/navigation.py create mode 100644 scripts/docs_skill/_helpers/output.py create mode 100644 scripts/docs_skill/_helpers/render.py create mode 100644 scripts/docs_skill/constants.py create mode 100644 scripts/docs_skill/main/__init__.py create mode 100644 scripts/docs_skill/main/check.py create mode 100644 scripts/docs_skill/main/generate.py create mode 100644 scripts/docs_skill/models.py create mode 100644 scripts/generate_docs_skill.py create mode 100644 website/src/content/docs/docs/cli/skills.mdx diff --git a/Cargo.lock b/Cargo.lock index 973e884..1c7ca93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -313,6 +313,25 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "include_dir" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "923d117408f1e49d914f1a379a309cffe4f18c05cf4e3d12e613a15fc81bd0dd" +dependencies = [ + "include_dir_macros", +] + +[[package]] +name = "include_dir_macros" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cab85a7ed0bd5f0e76d93846e0147172bed2e2d3f859bcc33a8d9699cad1a75" +dependencies = [ + "proc-macro2", + "quote", +] + [[package]] name = "indexmap" version = "2.14.2" @@ -618,6 +637,7 @@ dependencies = [ "clap", "globset", "ignore", + "include_dir", "rayon", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index ed21839..5da44b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ rust-version = "1.88" [workspace.dependencies] clap = { version = "4", features = ["derive"] } globset = "0.4" +include_dir = "0.7" ignore = "0.4" tempfile = "3" toml = "0.9" diff --git a/Makefile b/Makefile index 50dc0cc..a848e61 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ SHELL := /bin/bash -.PHONY: check check-ci check-rust develop-native test test-e2e test-integration test-rust test-unit verify +.PHONY: check check-ci check-docs-skill check-rust develop-native skills test test-e2e test-integration test-rust test-unit verify check: uv run ruff format . @@ -14,8 +14,15 @@ check-ci: uv run ruff check . uv run ty check src tests uv run fensu check + $(MAKE) --no-print-directory check-docs-skill $(MAKE) --no-print-directory check-rust +check-docs-skill: + uv run python -m scripts.check_docs_skill + +skills: + uv run python -m scripts.generate_docs_skill + check-rust: cargo fmt --check cargo clippy --all-targets --quiet -- -D warnings diff --git a/README.md b/README.md index d8ead1e..80d14da 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ sqlclean check [PATHS]... # report findings; exit 1 when there are any sqlclean fix [PATHS]... # apply safe fixes until stable, then format sqlclean format [PATHS]... # rewrite files in the canonical layout sqlclean rules [CODE] # list the Rules, or explain one +sqlclean skills # install the skill for AI coding agents ``` - With no paths, a command reads every matching file under the configuration root. diff --git a/crates/sqlclean-cli/Cargo.toml b/crates/sqlclean-cli/Cargo.toml index 60386b4..0709861 100644 --- a/crates/sqlclean-cli/Cargo.toml +++ b/crates/sqlclean-cli/Cargo.toml @@ -16,6 +16,7 @@ path = "src/main.rs" clap.workspace = true globset.workspace = true ignore.workspace = true +include_dir.workspace = true rayon.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/sqlclean-cli/build.rs b/crates/sqlclean-cli/build.rs new file mode 100644 index 0000000..032c5bd --- /dev/null +++ b/crates/sqlclean-cli/build.rs @@ -0,0 +1,5 @@ +//! Rebuild the executable when the bundled skill changes. + +fn main() { + println!("cargo:rerun-if-changed=skill"); +} diff --git a/crates/sqlclean-cli/skill/SKILL.md b/crates/sqlclean-cli/skill/SKILL.md new file mode 100644 index 0000000..69ee72e --- /dev/null +++ b/crates/sqlclean-cli/skill/SKILL.md @@ -0,0 +1,61 @@ +--- +name: sqlclean +description: ALWAYS load this skill when linting, fixing or formatting SQL with SQLClean, editing sqlclean.toml, writing or testing custom Rules (Python @rule functions with XSQC codes), recording Rule exceptions, or reading SQLClean findings (SQC codes, SQC999 parse errors) in CI, JSON or SARIF output. +--- + + +# SQLClean + +SQLClean is a fast SQL linter and formatter. `sqlclean check` reports Rule findings, +`sqlclean fix` applies their fixes and formats, and `sqlclean format` rewrites files in one +canonical layout. Custom Rules are plain Python functions over structural facts that the Rust +engine extracts from each statement. + +## The working loop + +1. **Orient.** Read the nearest `sqlclean.toml` (dialect, include and exclude, `[format]`, + `[rules]`). Paths in it are relative to its directory. +2. **Check.** `sqlclean check [PATHS]` exits 0 when clean, 1 with findings, 2 on errors. Use + `--output-format json` when you need to read findings programmatically. +3. **Explain.** `sqlclean rules SQC001` shows a Rule's message, fix safety and remediation. +4. **Fix.** `sqlclean fix --diff` previews safe fixes plus formatting; `sqlclean fix` writes them. + Fixes marked unsafe can change query results, such as `= NULL` becoming `IS NULL`. Never pass + `--unsafe-fixes` without the user's say-so. +5. **Format.** `sqlclean format --check` fails when a file needs formatting. Formatting is + idempotent, so rerunning it changes nothing. +6. **Recheck** until `sqlclean check` exits 0. + +## Rules of thumb + +- Fix the SQL rather than silencing a finding. When a finding is genuinely intended, record a + `[[rules.rule_exceptions]]` entry with the Rule, path and a `reason`. Use + `[[rules.rule_ignores]]` only for whole paths a Rule does not apply to. +- An exception that no longer matches a finding is reported as stale; remove it. +- `SQC999` is a parse error. It cannot be ignored: fix the SQL or set the right `dialect`, + globally or per path under `[[dialects]]`. +- `--select` and `--ignore` take codes or prefixes such as `SQC001` or `SQC`. +- Results are cached in `.sqlclean/cache/`. Use `--no-cache` only when debugging a custom Rule + that imports helpers from outside its `[rules] custom` paths. + +## Custom Rules + +- A custom Rule is a function decorated with `@rule` from `sqlclean`, in a file under a path + listed in `[rules] custom`. Its code is `XSQC`, optional capital letters, then three digits. +- The first keyword parameter names the subject: `statement: Statement`, `file: SqlFile` or + `repository: Repository`. The second is `ctx: RuleContext`. +- Return `ctx.finding(...)` values. Fixes are lists of `ctx.replace`, `ctx.insert_before`, + `ctx.insert_after` and `ctx.delete` edits, and the Rule declares `fixable`. +- Never print to standard output inside a Rule: the host speaks JSON over it. +- Test Rules with `sqlclean.testing.evaluate_rule` and `RuleCase`. +- `sqlclean facts --dialect duckdb < query.sql` prints the facts a Rule sees for a statement. + +## Exact syntax + +The bundled documentation in `references/docs/` matches the installed SQLClean version. Start +from `references/docs/CONTENTS.md` and open only the page you need: + +- Configuration keys: `references/docs/configuration.md` +- Every built-in Rule: `references/docs/rules/reference.md` +- Writing custom Rules: `references/docs/custom-rules/overview.md` and + `references/docs/custom-rules/statement-facts.md` +- Command flags: `references/docs/cli/` diff --git a/crates/sqlclean-cli/skill/references/docs/CONTENTS.md b/crates/sqlclean-cli/skill/references/docs/CONTENTS.md new file mode 100644 index 0000000..ea0132e --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/CONTENTS.md @@ -0,0 +1,39 @@ + + +# SQLClean documentation index + +Bundled copies of every page on the SQLClean documentation site, matching the installed SQLClean version. Open only the page you need. + +## Getting Started + +- [Introduction](index.md) (`index`) - Clean SQL, every time. Fast linting, safe fixes and one canonical format. +- [Quickstart](quickstart.md) (`quickstart`) - Install SQLClean, then check, fix and format your first SQL file. + +## Concepts + +- [Configuration](configuration.md) (`configuration`) - Every sqlclean.toml key, its default, per-path dialects and how the file is found. +- [Overview](rules/overview.md) (`rules/overview`) - Select Rules, record reviewed exceptions, and apply safe and unsafe fixes. +- [Rule reference](rules/reference.md) (`rules/reference`) - Every built-in Rule, whether it runs by default, and whether it has a fix. +- [Formatting](formatting.md) (`formatting`) - One canonical layout for every SQL file, the line width and indent, and files that can't be formatted. + +## Custom Rules + +- [Overview](custom-rules/overview.md) (`custom-rules/overview`) - Write your own Rules as Python functions over the facts of each SQL statement. +- [Statement facts](custom-rules/statement-facts.md) (`custom-rules/statement-facts`) - Every node type and field a custom Rule can read from a statement, a file and the repository. +- [Fixes and options](custom-rules/fixes-and-options.md) (`custom-rules/fixes-and-options`) - Give custom Rules safe or unsafe fixes, and typed options configured in sqlclean.toml. +- [Testing](custom-rules/testing.md) (`custom-rules/testing`) - Test custom Rules with the sqlclean.testing harness, and keep them deterministic. + +## Integrations + +- [Integrations](integrations.md) (`integrations`) - Run SQLClean in CI with JSON or SARIF output, in pre-commit, and from editors. +- [Embedding the engine](embedding.md) (`embedding`) - Lint, format and read statement facts from Rust, in a tool that owns its own configuration. +- [Benchmarks](benchmarks.md) (`benchmarks`) - SQLClean, sqruff and SQLFluff linting the same 10,282 Snowflake SQL files. + +## CLI Reference + +- [sqlclean check](cli/check.md) (`cli/check`) - Report Rule findings without changing any file. +- [sqlclean fix](cli/fix.md) (`cli/fix`) - Apply Rule fixes until the files stop changing, then format them. +- [sqlclean format](cli/format.md) (`cli/format`) - Rewrite SQL files in the canonical layout, or check that they already are. +- [sqlclean rules](cli/rules.md) (`cli/rules`) - List the built-in and custom Rules, or show one Rule's details. +- [sqlclean facts](cli/facts.md) (`cli/facts`) - Print the statement facts custom Rules read for SQL on standard input. +- [sqlclean skills](cli/skills.md) (`cli/skills`) - Install the SQLClean skill so AI coding agents know its commands, Rules and configuration. diff --git a/crates/sqlclean-cli/skill/references/docs/benchmarks.md b/crates/sqlclean-cli/skill/references/docs/benchmarks.md new file mode 100644 index 0000000..3b48db7 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/benchmarks.md @@ -0,0 +1,80 @@ + + +# Benchmarks + +> SQLClean, sqruff and SQLFluff linting the same 10,282 Snowflake SQL files. + +Online: https://sqlclean.io/docs/benchmarks/ + +SQLClean 0.0.1, sqruff 0.44.0 and SQLFluff 4.4.0 linting the same [10,282 SQL files](#the-corpus) on +a Linux machine with 4 CPUs. Each tool runs its default rules on the Snowflake dialect, writes JSON +findings, and starts with no cache. SQLClean and sqruff cells are the median of three rounds; +SQLFluff ran once. + +| Command | Wall time | CPU time | Peak memory | vs SQLClean defaults | +|---|---:|---:|---:|---:| +| `sqlclean check` (13 default Rules) | **0.81 s** | 2.5 s | 48 MiB | 1× | +| `sqlclean check --select SQC` (all 45 Rules) | **1.33 s** | 3.8 s | 140 MiB | 1.6× | +| `sqlclean check` with three [custom Rules](#custom-rules) | **2.27 s** | 6.9 s | 182 MiB | 2.8× | +| `sqlclean format --check` | **3.03 s** | 11.5 s | 49 MiB | 3.7× | +| `sqruff lint` | 41.0 s | 114 s | 235 MiB | 51× | +| `sqlfluff lint --processes 4` | 919 s | 2,796 s | 992 MiB | 1,135× | + +Run again with nothing changed, `sqlclean check` reads its result cache and finishes in 0.23 s. +sqruff and SQLFluff have no result cache, so a second run costs the same as the first. + +## Custom Rules + +The custom variant adds three Python Rules from `benchmarks/linters/rules/` to the default Rules: +marts must not read raw sources, staging models read only sources and seeds, and no model calls a +nondeterministic function such as `CURRENT_TIMESTAMP`. They report 3,355 findings on top of the +default Rules' 3,872. + +SQLClean builds each statement's facts in Rust and evaluates statement and file Rules in one Python +process per CPU, so custom Rules use every core. A Rule over the whole repository runs in one +process, because it reads every file. Run again with nothing changed, the custom variant reads its +result cache and finishes in 0.46 s. + +## Comparing like with like + +The tools don't check the same things. Almost every sqruff and SQLFluff finding on this corpus +comes from a layout rule, such as indentation (`LT02`) or line length (`LT05`). SQLClean leaves +layout to its [formatter](formatting.md), so the fair comparison is `sqlclean check` plus +`sqlclean format --check`: 3.8 s together, against 41 s for sqruff and 919 s for SQLFluff. + +The finding counts show that each tool read every file, not how many problems each one finds: + +| Tool | Findings | Parse errors | +|---|---:|---:| +| SQLClean, default Rules | 3,872 | 0 | +| SQLClean, all Rules | 34,322 | 0 | +| sqruff | 258,378 | 0 | +| SQLFluff | 266,367 | 0 | + +## The corpus + +The files come from DuckShop 10K, the generated project SQLBuild benchmarks its compiler with. It +describes a fictional home goods business: storefronts, marketplaces, carriers, ledgers, ad networks +and support tools, with every name invented. Compiling the project expands its macros, and +`benchmarks/linters/convert_duckshop.py` then replaces SQLBuild's reference functions with plain +table names, so every linter reads ordinary Snowflake SQL. + +| What | Count | +|---|---:| +| SQL files | 10,282 | +| Staging, intermediate and mart models | 6,149, 1,961 and 2,162 | +| Size | 44 MB | + +## Run it yourself + +The harness lives in `benchmarks/linters/`. Its README has the steps to build the corpus; +then: + +```bash +pip install sqruff sqlfluff +benchmarks/linters/bench.sh /tmp/corpus/sql /tmp/linter-bench 3 +python benchmarks/linters/summarize.py /tmp/linter-bench +``` + +Each round copies the corpus into a fresh directory and runs every tool in alternating order. +`/usr/bin/time` records wall time, CPU time and peak memory. diff --git a/crates/sqlclean-cli/skill/references/docs/cli/check.md b/crates/sqlclean-cli/skill/references/docs/cli/check.md new file mode 100644 index 0000000..8e0ff69 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/check.md @@ -0,0 +1,170 @@ + + +# sqlclean check + +> Report Rule findings without changing any file. + +Online: https://sqlclean.io/docs/cli/check/ + +## Contents + +- Usage +- Flags +- Text output +- JSON output +- SARIF output +- Exit codes + +`sqlclean check` runs the selected Rules over SQL files and reports every finding. It never changes +a file. + +`sqlclean lint` is the same command, for anyone used to SQLFluff or sqruff. + +## Usage + +```bash +sqlclean check [OPTIONS] [PATHS]... +``` + +```bash +sqlclean check # every matching file under the configuration root +sqlclean check models/orders.sql reports/ # only these files and directories +sqlclean check --select SQC001,SQC004 # only these Rules +sqlclean check --output-format sarif > sqlclean.sarif +``` + +With no paths, `check` reads every file under the configuration root that matches `include` and +not `exclude`. A directory is walked the same way. A file named on the command line is read even +when `include` does not match it, unless `exclude` does. See +[Choosing files](../configuration.md#choosing-files). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to read; the configuration root when omitted | +| `--select ` | Rule codes or prefixes to run instead of `[rules] select` | +| `--ignore ` | Rule codes or prefixes to skip, in addition to `[rules] ignore` | +| `--no-cache` | Evaluate every file instead of reusing results for unchanged inputs | +| `--output-format ` | `text` (default), `json` or `sarif` | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +`--select` and `--ignore` take a comma-separated list, or the flag repeated. A value is an exact +code such as `SQC001` or a prefix such as `SQC` or `XSQCNAME`. A value that matches no Rule is an +error. See [Selecting Rules](../rules/overview.md#selecting-rules). + +## Text output + +```text +error[SQC004]: Row selection is nondeterministic + --> models/customers.sql:6:1 + | + 6 | LIMIT 10; + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +error[SQC001]: Comparison with NULL is never true + --> models/orders.sql:8:22 + | + 8 | WHERE o.cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error: stale rule exception: SQC004 in models/orders.sql matches no finding; remove it from sqlclean.toml + +Found 3 problems (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +Findings are sorted by path, then position. Each one shows the Rule code and message, the +location as `path:line:column`, the source line, the remediation, and the command that applies its +fix when it has one. [Stale exceptions](../rules/overview.md#stale-exceptions) follow the +findings. A clean run prints `All checks passed.` + +Paths are relative to the configuration root, with `/` separators. Lines and columns start at 1 +and count characters, not bytes. + +## JSON output + +`--output-format json` prints one document: + +```json +{ + "findings": [ + { + "code": "SQC004", + "path": "models/customers.sql", + "line": 6, + "column": 1, + "end_line": 6, + "end_column": 6, + "message": "Row selection is nondeterministic", + "remediation": "Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET.", + "fix": null + }, + { + "code": "SQC001", + "path": "models/orders.sql", + "line": 8, + "column": 22, + "end_line": 8, + "end_column": 23, + "message": "Comparison with NULL is never true", + "remediation": "Use IS NULL or IS NOT NULL when testing for NULL.", + "fix": "unsafe" + } + ], + "stale_exceptions": [ + { + "rule": "SQC004", + "path": "models/orders.sql", + "relation": null + } + ], + "summary": { + "findings": 2, + "fixable": 1, + "stale_exceptions": 1 + } +} +``` + +`fix` is `"safe"`, `"unsafe"` or `null` when the finding has no fix. `summary.fixable` counts +findings with a fix of either kind. + +## SARIF output + +`--output-format sarif` prints a SARIF 2.1.0 log for code-scanning tools. Every finding is a result +with `level` `error`, its Rule code as `ruleId`, and its message and remediation as the result +text: + +```json +{ + "ruleId": "SQC001", + "level": "error", + "message": { + "text": "Comparison with NULL is never true. Use IS NULL or IS NOT NULL when testing for NULL." + }, + "locations": [ + { + "physicalLocation": { + "artifactLocation": { "uri": "models/orders.sql" }, + "region": { "startLine": 8, "startColumn": 22, "endLine": 8, "endColumn": 23 } + } + } + ] +} +``` + +The SARIF log lists findings only. Stale exceptions still make the command exit `1`; read them +from the text or JSON output. See [CI](../integrations.md#ci) for an upload example. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | No findings and no stale exceptions | +| `1` | At least one finding or stale exception | +| `2` | The command could not run: invalid arguments, configuration or input, or a custom Rule that failed | + +Errors are printed on stderr as `sqlclean: error: `. diff --git a/crates/sqlclean-cli/skill/references/docs/cli/facts.md b/crates/sqlclean-cli/skill/references/docs/cli/facts.md new file mode 100644 index 0000000..1016d5f --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/facts.md @@ -0,0 +1,78 @@ + + +# sqlclean facts + +> Print the statement facts custom Rules read for SQL on standard input. + +Online: https://sqlclean.io/docs/cli/facts/ + +`sqlclean facts` parses SQL from standard input and prints the structural facts of each statement +as one line of JSON. These are the facts a [custom Rule](../custom-rules/overview.md) receives, +so use it to see what a Rule will read before writing one. + +## Usage + +```bash +sqlclean facts [--dialect ] < query.sql +``` + +```bash +echo "SELECT o.order_id, COUNT(*) AS items FROM orders AS o JOIN order_items AS i ON i.order_id = o.order_id WHERE o.status = 'paid' GROUP BY o.order_id;" \ + | sqlclean facts --dialect duckdb +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--dialect ` | The dialect to parse with (default: `generic`); see [Dialects](../configuration.md#dialects) | + +`facts` reads no configuration file and no paths. + +## Output + +The output, pretty-printed here and trimmed to some of the columns: + +```json +{ + "statements": [ + { + "index": 0, + "kind": "select", + "start": 0, + "end": 146, + "terminated_end": 147, + "ctes": [], + "tables": [ + { "start": 42, "end": 48, "name": "orders", "qualified_name": "orders", "alias": "o" }, + { "start": 59, "end": 70, "name": "order_items", "qualified_name": "order_items", "alias": "i" } + ], + "joins": [ + { "start": 54, "end": 102, "kind": "INNER", "relation": "order_items", "has_condition": true } + ], + "stars": [], + "functions": [ + { "start": 19, "end": 24, "name": "COUNT" } + ], + "columns": [ + { "start": 7, "end": 17, "name": "order_id", "qualifier": "o" }, + { "start": 109, "end": 117, "name": "status", "qualifier": "o" } + ], + "literals": [ + { "start": 120, "end": 126, "kind": "string", "text": "'paid'" } + ] + } + ] +} +``` + +Offsets count characters from the start of the input, not bytes. Custom Rules receive the same +facts with `line`, `column` and `text` added to every node. See +[Statement facts](../custom-rules/statement-facts.md) for every field. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | The facts were printed | +| `2` | The SQL does not parse under the dialect, or the dialect is unknown | diff --git a/crates/sqlclean-cli/skill/references/docs/cli/fix.md b/crates/sqlclean-cli/skill/references/docs/cli/fix.md new file mode 100644 index 0000000..9e1e033 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/fix.md @@ -0,0 +1,99 @@ + + +# sqlclean fix + +> Apply Rule fixes until the files stop changing, then format them. + +Online: https://sqlclean.io/docs/cli/fix/ + +`sqlclean fix` applies the fixes of the selected Rules, checks again, and repeats until no fix +applies. It then formats every file it read and reports the findings that remain. + +## Usage + +```bash +sqlclean fix [OPTIONS] [PATHS]... +``` + +```bash +sqlclean fix # apply safe fixes, then format +sqlclean fix --diff # print the changes without writing them +sqlclean fix --unsafe-fixes # also apply fixes that can change query results +sqlclean fix models/orders.sql # only this file +``` + +Files are chosen as for [`sqlclean check`](check.md). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to read; the configuration root when omitted | +| `--unsafe-fixes` | Also apply fixes their Rules mark unsafe because they can change results | +| `--diff` | Print the changes as a unified diff instead of writing them | +| `--select ` | Rule codes or prefixes to run instead of `[rules] select` | +| `--ignore ` | Rule codes or prefixes to skip, in addition to `[rules] ignore` | +| `--no-cache` | Evaluate every file instead of reusing results for unchanged inputs | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +Only fixes of selected Rules apply. `sqlclean fix --select SQC005` removes unused CTEs and +formats the files, and leaves every other finding alone. + +## Output + +```text +Fixed 1 problem(s) and changed 1 file(s). +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:7:24 + | + 7 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +Found 1 problem (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +The summary line goes to stderr. The remaining findings go to stdout in the same format as +[`sqlclean check`](check.md#text-output), with positions in the fixed file. + +With `--diff`, nothing is written. `fix` prints a unified diff of every file it would change, +followed by `Would fix N problem(s) and change N file(s).`, and does not list the remaining +findings: + +```diff +--- a/models/orders.sql ++++ b/models/orders.sql +@@ -5,7 +5,7 @@ + FROM orders o + JOIN customers c + ON o.customer_id = c.customer_id +-WHERE o.cancelled_at = NULL ++WHERE o.cancelled_at IS NULL + GROUP BY + o.order_id, + c.customer_name +``` + +## How fixes are applied + +- Each pass checks the files, then applies every eligible fix whose edits do not overlap an earlier + fix in the same file. A skipped fix is reconsidered on the next pass. +- A pass that would turn a file that parses into one that does not is not applied to that file. +- `fix` stops when a pass applies nothing, and after at most 10 passes. +- Safe fixes always apply. Unsafe fixes apply only with `--unsafe-fixes`. See + [Safe and unsafe fixes](../rules/overview.md#safe-and-unsafe-fixes). +- Fixes from [custom Rules](../custom-rules/fixes-and-options.md) apply the same way. +- Every file is formatted after the last pass, whether or not a fix changed it. A file the + formatter cannot format is reported as `: could not format: ` and keeps its fixes. + +A file that does not parse gets no fixes and is reported as +[`SQC999`](../rules/overview.md#parse-errors-sqc999). + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Every file is fixed and formatted, and no finding or stale exception remains | +| `1` | Findings or stale exceptions remain, a file could not be formatted, or `--diff` found changes to make | +| `2` | The command could not run: invalid arguments, configuration or input, or a custom Rule that failed | diff --git a/crates/sqlclean-cli/skill/references/docs/cli/format.md b/crates/sqlclean-cli/skill/references/docs/cli/format.md new file mode 100644 index 0000000..6060937 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/format.md @@ -0,0 +1,77 @@ + + +# sqlclean format + +> Rewrite SQL files in the canonical layout, or check that they already are. + +Online: https://sqlclean.io/docs/cli/format/ + +`sqlclean format` rewrites SQL files in SQLClean's canonical layout. It runs no Rules and applies +no fixes; use [`sqlclean fix`](fix.md) for both. + +## Usage + +```bash +sqlclean format [OPTIONS] [PATHS]... +``` + +```bash +sqlclean format # rewrite every matching file +sqlclean format --check # fail when a file needs formatting +sqlclean format --diff reports/ # print the changes for one directory +``` + +Files are chosen as for [`sqlclean check`](check.md). Each file is formatted with its own +[dialect](../configuration.md#dialects) and the configured +[`line_width`](../formatting.md#line-width) and [`indent`](../formatting.md#indent). + +## Flags + +| Flag | Description | +|------|-------------| +| `PATHS...` | Files or directories to format; the configuration root when omitted | +| `--check` | Fail when a file needs formatting, without writing it | +| `--diff` | Print the changes as a unified diff instead of writing them | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +## Output + +```bash +sqlclean format --check +``` + +```text +j.sql: could not format: Parse error at line 1, column 16: Expected Colon, got RBrace ('}') near [{{ col }} from orders] +Would reformat: models_a.sql +1 file(s) would be reformatted, 2 file(s) already formatted. +``` + +`--check` names each file that would change. `--diff` prints a unified diff instead: + +```diff +--- a/models_a.sql ++++ b/models_a.sql +@@ -1 +1,5 @@ +-select order_id, amount from orders where amount > 0 ++SELECT ++ order_id, ++ amount ++FROM orders ++WHERE amount > 0 +``` + +Without either flag, files are rewritten in place and the summary reads +`N file(s) reformatted, N file(s) already formatted.` The summary and the `could not format` +lines go to stderr; the `Would reformat` lines and diffs go to stdout. + +A file the formatter cannot format, usually because it does not parse under its dialect, is left +unchanged and reported with the reason. See +[When a file can't be formatted](../formatting.md#when-a-file-cant-be-formatted). + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Every file is formatted, or was rewritten | +| `1` | A file could not be formatted, or `--check` or `--diff` found a file to change | +| `2` | The command could not run: invalid arguments, configuration or input | diff --git a/crates/sqlclean-cli/skill/references/docs/cli/rules.md b/crates/sqlclean-cli/skill/references/docs/cli/rules.md new file mode 100644 index 0000000..2b80af8 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/rules.md @@ -0,0 +1,122 @@ + + +# sqlclean rules + +> List the built-in and custom Rules, or show one Rule's details. + +Online: https://sqlclean.io/docs/cli/rules/ + +## Contents + +- Usage +- Flags +- Listing Rules +- Showing one Rule +- JSON output +- Exit codes + +`sqlclean rules` lists every Rule the configuration can select: the built-in Rules, then the custom +Rules under `[rules] custom`. With a code, it shows that Rule. + +## Usage + +```bash +sqlclean rules [OPTIONS] [CODE] +``` + +```bash +sqlclean rules # list every Rule +sqlclean rules SQC001 # show one Rule +sqlclean rules --json # every Rule as JSON +sqlclean rules --json SQC001 # one Rule as JSON +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `CODE` | Show this Rule's details; an exact code, not a prefix | +| `--json` | Print Rules as a JSON array | +| `--config ` | Use this configuration file instead of the nearest `sqlclean.toml` | + +## Listing Rules + +```text +SQC001 default Comparison with NULL is never true +SQC002 default Comma-separated sources create an implicit cartesian join +SQC003 default Non-cross join has no meaningful condition +SQC004 default Row selection is nondeterministic +... +SQC045 JSON payload is parsed more than once in one SELECT +XSQCNAME001 default Query reads a temporary table +XSQCSTYLE001 default COUNT(1) should be COUNT(*) +``` + +`default` marks the Rules that run when `[rules] select` is not set. See +[Default Rules](../rules/overview.md#default-rules) and the +[Rule reference](../rules/reference.md). + +## Showing one Rule + +```bash +sqlclean rules SQC001 +``` + +```text +SQC001: Comparison with NULL is never true +Subject: statement +Enabled by default: yes +Fixable: unsafe +Remediation: Use IS NULL or IS NOT NULL when testing for NULL. +``` + +A custom Rule also shows the Python module that defines it: + +```text +XSQCSTYLE001: COUNT(1) should be COUNT(*) +Subject: statement +Enabled by default: yes +Defined in: sql_rules.naming +Fixable: safe +Remediation: Write COUNT(*). +``` + +`Fixable` is left out when the Rule has no fix. + +## JSON output + +```bash +sqlclean rules --json XSQCNAME001 +``` + +```json +[ + { + "code": "XSQCNAME001", + "message": "Query reads a temporary table", + "remediation": "Read from the published table instead.", + "enabled_by_default": true, + "custom": true, + "subject": "statement", + "module": "sql_rules.naming" + } +] +``` + +| Field | Description | +|-------|-------------| +| `code` | The Rule code | +| `message` | The finding message | +| `remediation` | What to do about a finding | +| `enabled_by_default` | Whether the Rule runs when `[rules] select` is not set | +| `custom` | `true` for custom Rules | +| `subject` | `statement`, `file` or `repository` | +| `fixable` | `safe` or `unsafe`; absent when the Rule has no fix | +| `module` | The Python module of a custom Rule; absent for built-in Rules | + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | The Rules were printed | +| `2` | Unknown Rule code, or the configuration or a custom Rule module could not be loaded | diff --git a/crates/sqlclean-cli/skill/references/docs/cli/skills.md b/crates/sqlclean-cli/skill/references/docs/cli/skills.md new file mode 100644 index 0000000..336137f --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/cli/skills.md @@ -0,0 +1,61 @@ + + +# sqlclean skills + +> Install the SQLClean skill so AI coding agents know its commands, Rules and configuration. + +Online: https://sqlclean.io/docs/cli/skills/ + +`sqlclean skills` writes the SQLClean skill into your project, so AI coding agents such as Claude +Code, OpenCode and Cursor know how to check, fix and format SQL, configure `sqlclean.toml` and +write custom Rules. The skill ships inside the `sqlclean` package with a copy of every page of +these docs, so it always matches the installed version. + +## Usage + +```bash +sqlclean skills [--target ]... [--global] [--force] +``` + +```bash +# Install the agents and claude targets (default) +sqlclean skills + +# Install for Claude Code only +sqlclean skills --target claude + +# Install under your home directory for every project +sqlclean skills --global +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--target ` | The agent to install for: `agents`, `claude` or `opencode`. Repeat it for several. | +| `--global` | Install under the home directory instead of the configuration root | +| `--force` | Overwrite skill files that SQLClean did not generate | + +## Targets + +| Target | Project path | Global path | +|--------|--------------|-------------| +| `agents` | `.agents/skills/sqlclean/` | `~/.agents/skills/sqlclean/` | +| `claude` | `.claude/skills/sqlclean/` | `~/.claude/skills/sqlclean/` | +| `opencode` | `.opencode/skills/sqlclean/` | `~/.config/opencode/skills/sqlclean/` | + +Without `--target`, SQLClean installs `agents` and `claude`. OpenCode and several other agents +read the shared `.agents` directory; Claude Code needs its own `.claude` copy. + +The project path is relative to the directory of the nearest `sqlclean.toml`, or the working +directory when there is none. + +## Updating + +Every file SQLClean writes carries a `generated-by: sqlclean skills` comment. Rerun +`sqlclean skills` after upgrading SQLClean: + +- Generated files are rewritten with the new version's content. +- Generated pages the new version no longer ships are removed. +- A file without the comment, such as one you wrote yourself, is never overwritten. The command + stops with exit status 2 before writing anything, unless you pass `--force`. diff --git a/crates/sqlclean-cli/skill/references/docs/configuration.md b/crates/sqlclean-cli/skill/references/docs/configuration.md new file mode 100644 index 0000000..5a26208 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/configuration.md @@ -0,0 +1,188 @@ + + +# Configuration + +> Every sqlclean.toml key, its default, per-path dialects and how the file is found. + +Online: https://sqlclean.io/docs/configuration/ + +## Contents + +- Finding the file +- Keys +- Choosing files +- Dialects +- Validation + +SQLClean reads one `sqlclean.toml`. Every key is optional, and without a file every default +applies. + +```toml +dialect = "duckdb" # default "generic" +include = ["**/*.sql"] +exclude = ["vendor/**"] + +[[dialects]] # a different dialect for some paths +paths = ["warehouse/**"] +dialect = "snowflake" + +[format] +line_width = 100 +indent = 4 + +[rules] +select = ["SQC"] # omit to run the Rules enabled by default +ignore = ["SQC027"] +custom = ["sql_rules"] # Python files or directories that define custom Rules + +[rules.thresholds] +max_literal_length = 100 # SQC044 +max_ranking_order_by = 5 # SQC043 + +[rules.rule_options.XSQCNAME001] +prefixes = ["tmp_", "scratch_"] + +[[rules.rule_exceptions]] # one reviewed finding that is allowed +rule = "SQC038" +path = "models/calendar.sql" +relation = "dates" +reason = "Every pair of dates is needed." + +[[rules.rule_ignores]] # Rules that do not apply to some paths +rules = ["SQC034"] +paths = ["scratch/**"] +reason = "Exploratory queries." +``` + +## Finding the file + +Every command uses the nearest `sqlclean.toml` in the working directory or one of its parents. +`--config ` uses that file instead. + +The directory that holds the file is the **configuration root**. Every path in the file, every path +in findings, and the default set of files to read are relative to it. Without a file, the working +directory is the root. + +## Keys + +| Key | Default | Description | +|-----|---------|-------------| +| `dialect` | `"generic"` | The SQL dialect of every file without a `[[dialects]]` match | +| `include` | `["**/*.sql"]` | Glob patterns of the files to read | +| `exclude` | `[]` | Glob patterns of files never to read | +| `[[dialects]]` | none | `paths` and `dialect`: a different dialect for matching files | +| `[format] line_width` | `100` | The line width the formatter fills; see [Line width](formatting.md#line-width) | +| `[format] indent` | `4` | Spaces per indent step, from `1` to `8`; see [Indent](formatting.md#indent) | +| `[rules] select` | Rules enabled by default | Rule codes or prefixes to run | +| `[rules] ignore` | `[]` | Rule codes or prefixes to skip | +| `[rules] custom` | `[]` | Python files or directories that define [custom Rules](custom-rules/overview.md) | +| `[rules.thresholds] max_literal_length` | `100` | The longest string literal `SQC044` allows | +| `[rules.thresholds] max_ranking_order_by` | `5` | The most `ORDER BY` expressions `SQC043` allows in a ranking window | +| `[rules.rule_options.]` | none | Option values of one custom Rule; see [Options](custom-rules/fixes-and-options.md#options) | +| `[[rules.rule_exceptions]]` | none | One allowed finding; see [Exceptions](rules/overview.md#exceptions) | +| `[[rules.rule_ignores]]` | none | Rules that do not apply to some paths; see [Path ignores](rules/overview.md#path-ignores) | + +Selection, exceptions and ignores are described in [Rules](rules/overview.md). + +## Choosing files + +With no paths on the command line, a command reads every file under the configuration root whose +root-relative path matches `include` and no `exclude` pattern. + +- In patterns, `*` matches within one directory and `**` matches across directories: + `models/*.sql` reads `models/orders.sql` but not `models/marts/orders.sql`. +- In a Git repository, files that `.gitignore` ignores are skipped. +- Hidden files and directories, whose names start with `.`, are skipped. +- A directory on the command line is walked the same way. +- A file named on the command line is read even when `include` does not match it. `exclude` still + applies. + +## Dialects + +`dialect` sets how every file is parsed and formatted. `[[dialects]]` entries set a different +dialect for the files their `paths` match. The first matching entry wins; files that match none use +`dialect`. + +```toml +dialect = "duckdb" + +[[dialects]] +paths = ["reports/**"] +dialect = "tsql" +``` + +A file that does not parse under its dialect is reported as +[`SQC999`](rules/overview.md#parse-errors-sqc999), with the dialect in the message. Without +the `[[dialects]]` entry above, this T-SQL file under `reports/` is parsed as `duckdb`: + +```sql +SELECT [order id] FROM orders +``` + +```text +error[SQC999]: SQL could not be parsed as duckdb: Parse error at line 1, column 17: Expected RBracket, got Var ('id') near [SELECT [order id] FROM orders] +``` + +Supported dialects, case-insensitive: + +| Dialect | Also accepted | +|---------|---------------| +| `generic` | | +| `athena` | | +| `bigquery` | | +| `clickhouse` | | +| `cockroachdb` | `cockroach` | +| `databricks` | | +| `datafusion` | `arrow-datafusion`, `arrow_datafusion` | +| `doris` | | +| `dremio` | | +| `drill` | | +| `druid` | | +| `duckdb` | | +| `dune` | | +| `exasol` | | +| `fabric` | | +| `hana` | `saphana`, `sap_hana` | +| `hive` | | +| `materialize` | | +| `mysql` | | +| `oracle` | | +| `postgres` | `postgresql` | +| `presto` | | +| `redshift` | | +| `risingwave` | | +| `singlestore` | `memsql` | +| `snowflake` | | +| `solr` | | +| `spark` | `spark2` | +| `sqlite` | | +| `starrocks` | | +| `tableau` | | +| `teradata` | | +| `tidb` | | +| `trino` | | +| `tsql` | `mssql`, `sqlserver` | +| `vertica` | | + +## Validation + +SQLClean reads and validates the file when a command starts, and exits with `2` on the first +problem it finds. `check` and `fix` also validate Rule selectors and `rule_options` before they +report any finding. + +| Problem | Error | +|---------|-------| +| A key SQLClean does not know | ``unknown field `colour`, expected one of `dialect`, `include`, ...`` | +| An unknown dialect | `sqlclean.toml: unknown SQL dialect 'nosuch': ...` | +| An invalid glob pattern | `sqlclean.toml: invalid path pattern '[bad': ...` | +| `line_width` of `0` | `sqlclean.toml: [format] line_width must be positive` | +| `indent` of `0` or more than `8` | `sqlclean.toml: [format] indent must be between 1 and 8` | +| A threshold of `0` | `sqlclean.toml: [rules.thresholds] max_literal_length and max_ranking_order_by must be positive` | +| An exception or ignore without a reason | `sqlclean.toml: rule exception or ignore for SQC001 needs a reason` | +| An exception with `cte` but no `relation` | `sqlclean.toml: rule exception for SQC038 at models/calendar.sql sets cte without relation` | +| A `custom` path that does not exist | `[rules] custom path does not exist: sql_rules` | +| `rule_options` for a built-in Rule | `[rules.rule_options.SQC001]: built-in Rules take no options; set limits in [rules.thresholds]` | +| `rule_options` for a code no custom Rule has | `[rules.rule_options.XSQCNOPE001]: no custom Rule has this code` | +| A selector that matches no Rule | `Rule selector 'SQC9' matches no Rule` | + +Errors are printed as `sqlclean: error: `. diff --git a/crates/sqlclean-cli/skill/references/docs/custom-rules/fixes-and-options.md b/crates/sqlclean-cli/skill/references/docs/custom-rules/fixes-and-options.md new file mode 100644 index 0000000..e3f6c1f --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/custom-rules/fixes-and-options.md @@ -0,0 +1,160 @@ + + +# Fixes and options + +> Give custom Rules safe or unsafe fixes, and typed options configured in sqlclean.toml. + +Online: https://sqlclean.io/docs/custom-rules/fixes-and-options/ + +## Contents + +- Fixes +- Options + +A custom Rule can attach a fix to each finding, which [`sqlclean fix`](../cli/fix.md) applies, +and can declare typed options that a project sets in `sqlclean.toml`. + +## Fixes + +A fix is a list of edits to the file. Declare `fixable` on the Rule, then pass the edits to +`ctx.finding`: + +```python +from sqlclean import FixSafety, Finding, RuleContext, Statement, rule + +@rule( + code="XSQCSTYLE001", + message="COUNT(1) should be COUNT(*)", + remediation="Write COUNT(*).", + fixable=FixSafety.SAFE, + enabled_by_default=True, +) +def count_one(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + counts = {f.end + 1 for f in statement.functions if f.name.upper() == "COUNT"} + return [ + ctx.finding(subject=statement, node=literal, fix=[ctx.replace(node=literal, text="*")]) + for literal in statement.literals + if literal.text == "1" and literal.start in counts + ] +``` + +A function call is located at its name, so `f.end + 1` is the first character inside its +parentheses. + +```bash +sqlclean fix --diff --select XSQCSTYLE +``` + +```diff +--- a/models/orders.sql ++++ b/models/orders.sql +@@ -1,3 +1,6 @@ +-SELECT customer_id, COUNT(1) AS orders ++SELECT ++ customer_id, ++ COUNT(*) AS orders + FROM customer_orders +-GROUP BY customer_id ++GROUP BY ++ customer_id +Would fix 1 problem(s) and change 1 file(s). +``` + +### Making edits + +| Method | Edit | +|--------|------| +| `ctx.replace(node=..., text=...)` | Replace the node's text | +| `ctx.insert_before(node=..., text=...)` | Insert text just before the node | +| `ctx.insert_after(node=..., text=...)` | Insert text just after the node | +| `ctx.delete(node=...)` | Remove the node's text | + +`node` is any node or statement, or anything with `start` and `end` character offsets in the file. +An `Edit(start=..., end=..., text=...)` from `sqlclean` builds an edit from offsets directly. Every +edit must lie inside the finding's file. + +### Safe and unsafe + +`fixable=FixSafety.SAFE` declares fixes that keep the query's results; `sqlclean fix` applies them. +`fixable=FixSafety.UNSAFE` declares fixes that can change results; they apply only with +`--unsafe-fixes`. The safety belongs to the Rule, so every fix of one Rule has the same safety. +See [Safe and unsafe fixes](../rules/overview.md#safe-and-unsafe-fixes). + +A finding without `fix` has no fix, even in a fixable Rule. A Rule that returns a fix without +declaring `fixable` fails with `Rule XSQCSTYLE001 returned a fix but does not declare fixable`. + +### How fixes combine + +`sqlclean fix` applies custom fixes exactly as it applies built-in ones: + +- A fix whose edits overlap another fix's edits waits for the next pass, where the Rule runs again + on the changed file. +- A pass that would leave a file that no longer parses is not applied to that file. +- After at most 10 passes, the files are formatted, so a fix need not produce formatted SQL. + +Keep each edit as small as the change: replace the literal, not the statement. + +## Options + +An option is a typed value a project sets per Rule. Declare it with `RuleOption`, list it in +`options`, and read it with `ctx.option`: + +```python +from sqlclean import Finding, RuleContext, RuleOption, Statement, rule + +PREFIXES = RuleOption.string_list( + name="prefixes", default=("tmp_",), description="Table prefixes that must not be read." +) + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + options=(PREFIXES,), + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + prefixes = ctx.option(PREFIXES) + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith(prefixes) + ] +``` + +```toml +[rules.rule_options.XSQCNAME001] +prefixes = ["tmp_", "scratch_"] +``` + +`ctx.option` returns the configured value, or the default when `sqlclean.toml` does not set it. +List options arrive as tuples. + +| Constructor | TOML value | Extra arguments | +|-------------|------------|-----------------| +| `RuleOption.boolean(name=, default=, description=)` | `true` or `false` | | +| `RuleOption.integer(name=, default=, description=)` | An integer | `minimum`, `maximum` | +| `RuleOption.string(name=, default=, description=)` | A string | `choices` | +| `RuleOption.string_list(name=, default=, description=)` | A list of strings | | +| `RuleOption.integer_list(name=, default=, description=)` | A list of integers | | + +`name` must be a Python identifier, and the default must be valid for the option. + +### Validation + +Configured values are checked against the declarations, and a bad value stops the command with +exit code `2`: + +| Problem | Error | +|---------|-------| +| A value of the wrong type | `[rules.rule_options.XSQCNAME001] option prefixes expects a list, got 'tmp_'` | +| An integer out of bounds | `[rules.rule_options.XSQCFILE002] option max_statements must be at least 1` | +| A string outside `choices` | `[rules.rule_options.XSQCFILE002] option style must be one of ['strict', 'loose']` | +| A name the Rule does not declare | `Rule XSQCNAME001 has no option prefix` | +| A code no custom Rule has | `[rules.rule_options.XSQCNOPE001]: no custom Rule has this code` | +| A Rule that reads an option it did not declare | `Rule XSQCFILE002 reads undeclared option verbose` | + +Built-in Rules take no options. Their two limits are set in +[`[rules.thresholds]`](../rules/overview.md#thresholds). + +Changing an option evaluates that Rule again on every file; cached results for other Rules stay. diff --git a/crates/sqlclean-cli/skill/references/docs/custom-rules/overview.md b/crates/sqlclean-cli/skill/references/docs/custom-rules/overview.md new file mode 100644 index 0000000..7793d4e --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/custom-rules/overview.md @@ -0,0 +1,194 @@ + + +# Overview + +> Write your own Rules as Python functions over the facts of each SQL statement. + +Online: https://sqlclean.io/docs/custom-rules/overview/ + +## Contents + +- Where Rules live +- Declaring a Rule +- Choosing a subject +- Reporting findings +- How Rules run +- Python environment +- Errors +- Continue + +A custom Rule is a Python function decorated with `@rule`. It receives a statement, a file or the +whole repository, and returns findings. Custom Rules use the same selection, exceptions, fixes, +output and cache as the built-in Rules. + +```python +# sql_rules/naming.py +from sqlclean import Finding, RuleContext, Statement, rule + +@rule( + code="XSQCNAME001", + message="Query reads a temporary table", + remediation="Read from the published table instead.", + enabled_by_default=True, +) +def temporary_table(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + return [ + ctx.finding(subject=statement, node=table) + for table in statement.tables + if table.name.startswith("tmp_") + ] +``` + +Point `[rules] custom` at the file or its directory: + +```toml +[rules] +custom = ["sql_rules"] +``` + +```text +error[XSQCNAME001]: Query reads a temporary table + --> models/orders.sql:2:6 + | + 2 | FROM tmp_orders + | ^^^^^^^^^^ + = help: Read from the published table instead. +``` + +## Where Rules live + +`[rules] custom` lists Python files or directories, relative to the configuration root. SQLClean +imports every `.py` file under them and registers each function decorated with `@rule`. Other +functions, constants and classes stay ordinary Python. + +- Files named `test_*.py`, `conftest.py` and `__init__.py` are not loaded as Rules, so tests can + live next to the Rules they test. +- Modules are imported by their path from the configuration root: `sql_rules/naming.py` is + `sql_rules.naming`. A Rule module can import a helper the same way, for example + `from sql_rules.helpers import table_prefixes`. +- Two Rules with the same code are an error: `custom Rule code XSQCNAME001 is defined twice`. + +## Declaring a Rule + +| `@rule` argument | Default | Description | +|------------------|---------|-------------| +| `code` | required | `XSQC`, optional capital letters, and three digits, such as `XSQCNAME001` | +| `message` | required | The finding message | +| `remediation` | required | What to do about a finding | +| `enabled_by_default` | `False` | Run the Rule when `[rules] select` is not set | +| `fixable` | `None` | `FixSafety.SAFE` or `FixSafety.UNSAFE` when findings carry fixes | +| `options` | `()` | Typed `RuleOption`s configured in `sqlclean.toml` | +| `slug` | the code in lower case | A short name for the Rule | + +A Rule that is not enabled by default runs when `select` names it, by code or by a prefix such as +`XSQC` or `XSQCNAME`. See [Selecting Rules](../rules/overview.md#selecting-rules). Fixes and +options are covered in [Fixes and options](fixes-and-options.md). + +## Choosing a subject + +The function takes exactly two keyword-only parameters: one subject and one `RuleContext`. Their +type annotations decide how the Rule is called; their names and order do not matter. + +| Subject | Called | Use it for | +|---------|--------|------------| +| `statement: Statement` | Once per statement | Checks of one query, such as the tables it reads | +| `file: SqlFile` | Once per file | Checks of a file as a whole, such as how many statements it holds | +| `repository: Repository` | Once over every checked file | Conventions across files | + +```python +from collections import Counter + +from sqlclean import Finding, Repository, RuleContext, SqlFile, rule + +@rule( + code="XSQCFILE001", + message="File holds more than one statement", + remediation="Split the file so each holds one statement.", +) +def one_statement(*, file: SqlFile, ctx: RuleContext) -> list[Finding]: + return [ctx.finding(subject=file)] if len(file.statements) > 1 else [] + +@rule( + code="XSQCREPO001", + message="Two files define a CTE with the same name", + remediation="Give each CTE a name that is unique across files.", +) +def duplicate_cte_names(*, repository: Repository, ctx: RuleContext) -> list[Finding]: + statements = [s for file in repository.files for s in file.statements] + counts = Counter(cte.name for s in statements for cte in s.ctes) + return [ + ctx.finding(subject=s, node=cte) + for s in statements + for cte in s.ctes + if counts[cte.name] > 1 + ] +``` + +A Repository Rule is evaluated again whenever any checked file changes, so prefer a statement or +file subject when the check needs only one file. + +[Statement facts](statement-facts.md) lists everything a subject exposes. + +## Reporting findings + +A Rule returns a list of findings made with `ctx.finding`: + +| Argument | Description | +|----------|-------------| +| `subject` | The `Statement` or `SqlFile` the finding is in | +| `node` | Where to report it: any node or statement; the start of `subject` when omitted | +| `message` | Replaces the Rule's message for this finding | +| `remediation` | Replaces the Rule's remediation for this finding | +| `fix` | A list of edits; see [Fixes](fixes-and-options.md#fixes) | + +A Repository Rule passes the statement or file each finding belongs to as `subject`. + +## How Rules run + +- Custom Rules run after the built-in Rules, on files that parse. A file with a + [parse error](../rules/overview.md#parse-errors-sqc999) gets no custom Rule findings. +- From 500 files, Statement and File Rules split the files across one Python process per CPU. A + Rule can't share state between files, so anything that spans files belongs in a Repository Rule, + which always runs in one process with every file. +- The working directory is the configuration root, and `PYTHONHASHSEED` is `0`, so iterating a set + of strings gives the same order on every run. +- Results are cached with the built-in results. Editing any Python file under `[rules] custom` + evaluates the custom Rules again. See [Caching](../rules/overview.md#caching). +- Don't print to standard output from a Rule. The command reads Rule results from it and fails + with `custom Rules host returned no result`. + +## Python environment + +Custom Rules run in a Python interpreter that has the `sqlclean` package installed. SQLClean uses, +in order: + +1. the interpreter named by the `SQLCLEAN_PYTHON` environment variable; +2. the Python installed next to the `sqlclean` executable, such as the one in the same virtual + environment; +3. `python3` on the `PATH`. + +When none has the package, the command fails with: + +```text +Custom Rules need the sqlclean Python package in the environment that runs sqlclean: install it with `pip install sqlclean`, or set SQLCLEAN_PYTHON to an interpreter that has it. +``` + +## Errors + +A Rule that cannot load or run stops the command with exit code `2`: + +| Problem | Error | +|---------|-------| +| An invalid code | `custom Rule code must be XSQC, optional capital letters and 3 digits, such as XSQCNAME001: XSQC01` | +| A wrong signature | `custom Rule XSQCBAD001 must accept exactly one typed Statement, SqlFile or Repository subject and one typed RuleContext, both keyword-only` | +| A module that fails to import | `could not import custom Rule module sql_rules.bad: ` | +| An exception in the Rule | `custom Rule XSQCBAD001 failed on models/a.sql: IndexError: tuple index out of range`, then the traceback | +| A return value that is not a list of findings | `custom Rule XSQCRET001 must return a list of findings from ctx.finding(...)` | +| A fix from a Rule without `fixable` | `Rule XSQCBAD001 returned a fix but does not declare fixable` | + +## Continue + + Every node type and field a Rule can read. + Add safe or unsafe fixes, and typed options set in `sqlclean.toml`. + Test Rules with the `sqlclean.testing` harness. + See the facts of any SQL from the command line. diff --git a/crates/sqlclean-cli/skill/references/docs/custom-rules/statement-facts.md b/crates/sqlclean-cli/skill/references/docs/custom-rules/statement-facts.md new file mode 100644 index 0000000..49828ae --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/custom-rules/statement-facts.md @@ -0,0 +1,186 @@ + + +# Statement facts + +> Every node type and field a custom Rule can read from a statement, a file and the repository. + +Online: https://sqlclean.io/docs/custom-rules/statement-facts/ + +## Contents + +- Positions +- Statement +- Cte +- Table +- Join +- Star +- FunctionCall +- ColumnReference +- Literal +- SqlFile +- Repository +- RuleContext + +A custom Rule reads structural facts that the native engine extracts when it parses each file: the +CTEs, tables, joins, stars, function calls, columns and literals of every statement. Every node +carries its position, so a finding can point at it and a fix can replace it. + +```python +from sqlclean import Finding, RuleContext, Statement, rule + +@rule( + code="XSQCJOIN001", + message="Join to customers has no condition", + remediation="Join customers on customer_id.", +) +def unkeyed_customer_join(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + return [ + ctx.finding(subject=statement, node=join) + for join in statement.joins + if join.relation == "customers" and not join.has_condition + ] +``` + +To see the facts of any SQL, pipe it to [`sqlclean facts`](../cli/facts.md). + +## Positions + +Every node, and every statement, has these fields: + +| Field | Type | Description | +|-------|------|-------------| +| `start` | `int` | Offset of the first character in the file | +| `end` | `int` | Offset just past the last character | +| `line` | `int` | Line of `start`, from 1 | +| `column` | `int` | Column of `start`, from 1 | +| `text` | `str` | The source text from `start` to `end` | + +Offsets count Unicode characters from the start of the file, not bytes, so +`file.source[node.start:node.end] == node.text` in Python. Offsets are in the file, not the +statement: the second statement of a file starts after the first. + +## Statement + +One statement of a file, without its terminating semicolon. + +| Field | Type | Description | +|-------|------|-------------| +| `path` | `str` | The file's path relative to the configuration root | +| `index` | `int` | The statement's position in the file, from 0 | +| `kind` | `str` | The statement kind in lower case, such as `select`, `union`, `insert`, `update`, `delete` or `create_table` | +| `dialect` | `str` | The dialect the file was parsed with | +| `ctes` | `tuple[Cte, ...]` | Common table expressions | +| `tables` | `tuple[Table, ...]` | Relations read in `FROM` and `JOIN` | +| `joins` | `tuple[Join, ...]` | Joins | +| `stars` | `tuple[Star, ...]` | `*` and `qualifier.*` projections | +| `functions` | `tuple[FunctionCall, ...]` | Calls of named functions | +| `columns` | `tuple[ColumnReference, ...]` | Column references | +| `literals` | `tuple[Literal, ...]` | String and numeric literals | + +Each tuple is in source order and includes nodes inside CTEs and subqueries. + +`statement.polyglot_ast()` returns the full Polyglot syntax tree of the statement, parsed on first +use. It needs the `ast` extra: `pip install "sqlclean[ast]"`. Without it, the call fails with +`polyglot_ast() needs the Polyglot SQL package: install the ast extra: pip install 'sqlclean[ast]'`. + +## Cte + +A common table expression, located at its name. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The CTE name | + +## Table + +A relation read in `FROM` or `JOIN`, located at its name. A CTE that the query reads appears here +too. The target of `INSERT INTO`, `UPDATE` and `DELETE FROM` does not. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The relation name without its schema, such as `payments` | +| `qualified_name` | `str` | The name as qualified in the SQL, such as `shop.payments` | +| `alias` | `str \| None` | The alias, or `None` | + +## Join + +One join, from its join keyword to the end of its condition, such as +`LEFT JOIN paid ON o.order_id = paid.order_id`. + +| Field | Type | Description | +|-------|------|-------------| +| `kind` | `str` | The join kind in upper case: `INNER` for a bare `JOIN`, `LEFT`, `FULL`, `CROSS`, `NATURAL` and so on | +| `relation` | `str \| None` | The joined relation's name, or `None` when the join reads a subquery | +| `has_condition` | `bool` | Whether the join has `ON` or `USING` | + +A join that holds no table or column reference, such as `CROSS JOIN UNNEST([1, 2])`, is not +listed. + +## Star + +A `*` or `qualifier.*` projection. + +| Field | Type | Description | +|-------|------|-------------| +| `qualifier` | `str \| None` | The qualifier of `o.*`, or `None` for a bare `*` | + +## FunctionCall + +A call of a named function, located at its name. The arguments follow at `end + 1`, after the +opening parenthesis. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The name as written: `count` and `COUNT` differ | + +## ColumnReference + +A column reference, including its qualifier: the text of `o.order_id` is `o.order_id`. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | `str` | The column name, without quotes | +| `qualifier` | `str \| None` | The relation or alias that qualifies it, or `None` | + +## Literal + +A string or numeric literal as written. `TRUE`, `FALSE` and `NULL` are not literals here. + +| Field | Type | Description | +|-------|------|-------------| +| `kind` | `str` | `string` or `number` | + +`text` holds the literal with its quotes: `'paid'`, `1.5`. + +## SqlFile + +The subject of a file Rule, and the parent of every statement. + +| Field | Type | Description | +|-------|------|-------------| +| `path` | `str` | The path relative to the configuration root | +| `source` | `str` | The file's full text | +| `dialect` | `str` | The dialect the file was parsed with | +| `statements` | `tuple[Statement, ...]` | The statements, in order | + +## Repository + +The subject of a repository Rule. + +| Field | Type | Description | +|-------|------|-------------| +| `files` | `tuple[SqlFile, ...]` | Every checked file that parses, sorted by path | + +## RuleContext + +The second parameter of every Rule. + +| Member | Description | +|--------|-------------| +| `ctx.file` | The `SqlFile` being checked, or `None` in a repository Rule | +| `ctx.dialect` | The dialect of that file, or `None` in a repository Rule | +| `ctx.option(OPTION)` | The configured value of a declared option; see [Options](fixes-and-options.md#options) | +| `ctx.finding(...)` | A finding; see [Reporting findings](overview.md#reporting-findings) | +| `ctx.replace`, `ctx.insert_before`, `ctx.insert_after`, `ctx.delete` | Fix edits; see [Fixes](fixes-and-options.md#fixes) | + +All facts are read-only. A Rule cannot change the SQL except through the edits of a fix. diff --git a/crates/sqlclean-cli/skill/references/docs/custom-rules/testing.md b/crates/sqlclean-cli/skill/references/docs/custom-rules/testing.md new file mode 100644 index 0000000..2c10112 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/custom-rules/testing.md @@ -0,0 +1,183 @@ + + +# Testing + +> Test custom Rules with the sqlclean.testing harness, and keep them deterministic. + +Online: https://sqlclean.io/docs/custom-rules/testing/ + +## Contents + +- Running the tests +- RuleCase +- RuleResult +- Failures +- Determinism + +`sqlclean.testing` runs one Rule over SQL you give it, with the same native engine as +`sqlclean check`. It fails when the Rule reports a different number of findings than you expect, +or when its findings depend on the order of the files. + +```python +# sql_rules/test_naming.py +from sqlclean.testing import RuleCase, evaluate_rule + +from sql_rules.naming import count_one, temporary_table + +def test_scratch_tables_are_reported() -> None: + result = evaluate_rule( + rule=temporary_table, + test_case=RuleCase( + description="reads scratch_orders", + source="SELECT order_id FROM scratch_orders", + dialect="duckdb", + options={"prefixes": ["tmp_", "scratch_"]}, + expected_finding_count=1, + ), + ) + finding = result.findings[0] + assert (finding.start, finding.end) == (21, 35) + +def test_published_tables_pass() -> None: + evaluate_rule( + rule=temporary_table, + test_case=RuleCase( + description="reads orders", + source="SELECT order_id FROM orders", + expected_finding_count=0, + ), + ) + +def test_count_one_is_fixed() -> None: + result = evaluate_rule( + rule=count_one, + test_case=RuleCase( + description="COUNT(1)", + source="SELECT COUNT(1) FROM orders", + expected_finding_count=1, + ), + ) + assert result.fixed_source == "SELECT COUNT(*) FROM orders" +``` + +Test failing cases, passing cases and near misses, and assert exact positions where they matter. + +## Running the tests + +Files named `test_*.py` and `conftest.py` under `[rules] custom` are not loaded as Rules, so tests +can live next to the Rules they test. They import Rules by module path from the configuration +root, so put the root on the import path: run `python -m pytest` from the root, add a +`conftest.py` there, or set `pythonpath`: + +```toml +# pyproject.toml +[tool.pytest.ini_options] +pythonpath = ["."] +``` + +The harness runs the `sqlclean` executable installed in the same environment, or the one on the +`PATH`, to parse the case SQL. + +## RuleCase + +| Field | Default | Description | +|-------|---------|-------------| +| `description` | required | Names the case in failure messages | +| `source` | required | The SQL of the file under test | +| `expected_finding_count` | required | How many findings the Rule must report over every file of the case | +| `path` | `"query.sql"` | The path of `source` | +| `dialect` | `"generic"` | The dialect every file of the case is parsed with | +| `files` | `()` | More files, as `RuleFile(path=..., source=...)` | +| `options` | `{}` | Option values, as in `[rules.rule_options.]` | + +`options` are validated as in `sqlclean.toml`, so a test with a bad value fails with the same +error as `sqlclean check`. See [Validation](fixes-and-options.md#validation). + +`files` matter for file and repository Rules. A repository Rule sees every file of the case: + +```python +from sqlclean.testing import RuleCase, RuleFile, evaluate_rule + +from sql_rules.layout import duplicate_cte_names + +def test_cte_names_repeated_across_files() -> None: + result = evaluate_rule( + rule=duplicate_cte_names, + test_case=RuleCase( + description="paid is defined in two files", + source="WITH paid AS (SELECT order_id FROM payments) SELECT order_id FROM paid", + path="models/paid_orders.sql", + files=( + RuleFile( + path="models/refunds.sql", + source="WITH paid AS (SELECT order_id FROM refunds) SELECT order_id FROM paid", + ), + ), + expected_finding_count=2, + ), + ) + assert [finding.path for finding in result.findings] == [ + "models/paid_orders.sql", + "models/refunds.sql", + ] +``` + +## RuleResult + +| Member | Description | +|--------|-------------| +| `findings` | The findings, sorted by path and position | +| `finding_count` | `len(findings)` | +| `fixed_source` | `source` with the fixes of its findings applied; `source` itself when there are none | + +Each `Finding` has `code`, `path`, `start`, `end`, `message`, `remediation` and `edits`. Offsets +count characters in the file, as in [Statement facts](statement-facts.md#positions). + +`fixed_source` applies every finding's edits once, skipping an edit that overlaps an earlier one, +and does not format the result. It shows exactly what the Rule's fix changes. + +## Failures + +`evaluate_rule` raises `RuleCaseError`, which fails the test: + +| Problem | Message | +|---------|---------| +| A different number of findings | `reads orders: expected 1 findings, got 0` | +| Findings that depend on file order | `custom Rule XSQCND001 is not deterministic` | +| Case SQL that does not parse | `sqlclean could not parse the case SQL: sqlclean: error: Parse error at line 1, column 18: ...` | +| A function without `@rule` | `evaluate_rule needs a function decorated with @rule` | +| No `sqlclean` executable | `the sqlclean executable is not installed; install sqlclean` | + +Errors in the Rule itself, such as an exception or a fix without `fixable`, raise the same errors +as in [`sqlclean check`](overview.md#errors). + +## Determinism + +`sqlclean check` caches each Rule's findings per file and reuses them while the inputs are +unchanged, so a Rule must report the same findings for the same input every time. The harness +checks this: it evaluates the Rule over the case's files, then again in reverse file order, and +fails when the two sets of findings differ. + +The usual cause is state kept between calls, such as a module-level set of names already seen: + +```python +SEEN: set[str] = set() + +@rule(code="XSQCND001", message="Table read in an earlier file", remediation="...") +def seen_before(*, statement: Statement, ctx: RuleContext) -> list[Finding]: + found = [ctx.finding(subject=statement, node=t) for t in statement.tables if t.name in SEEN] + SEEN.update(t.name for t in statement.tables) + return found +``` + +```text +RuleCaseError: custom Rule XSQCND001 is not deterministic +``` + +Compute findings from the subject and `ctx` alone. When a Rule needs to compare files, give it a +[repository subject](overview.md#choosing-a-subject), which sees every file at +once. + +Avoid other inputs SQLClean does not track, such as the clock, environment variables, network +calls and files read directly from disk. A changed input of that kind does not invalidate cached +results; run with `--no-cache` when it changes. diff --git a/crates/sqlclean-cli/skill/references/docs/embedding.md b/crates/sqlclean-cli/skill/references/docs/embedding.md new file mode 100644 index 0000000..e8e4a90 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/embedding.md @@ -0,0 +1,160 @@ + + +# Embedding the engine + +> Lint, format and read statement facts from Rust, in a tool that owns its own configuration. + +Online: https://sqlclean.io/docs/embedding/ + +## Contents + +- Adding the dependency +- Entry points +- Lint requests +- Fixes +- Formatting +- Statement facts +- What stays in the host + +The `sqlclean` crate is the engine without the command: the built-in Rules, the formatter and the +statement facts. Tools that template, compile or generate SQL can call it in process and keep their +own configuration, file discovery and Rule codes. + +```rust +use sqlclean::sql_lint::constants::LINT_API_VERSION; +use sqlclean::sql_lint::main::lint::lint; +use sqlclean::sql_lint::models::{LintRequest, LintResponse}; + +fn main() -> Result<(), String> { + let request = LintRequest { + version: LINT_API_VERSION, + sql: "SELECT order_id FROM orders WHERE cancelled_at = NULL LIMIT 10".to_owned(), + dialect: "duckdb".to_owned(), + enabled_rules: Some(vec!["SQC001".to_owned(), "SQC004".to_owned()]), + ..LintRequest::default() + }; + let response: LintResponse = lint(request)?; + for diagnostic in &response.diagnostics { + println!("{} {}..{} {}", diagnostic.code, diagnostic.start, diagnostic.end, diagnostic.message); + if let Some(fix) = &diagnostic.fix { + println!(" fix: replace {}..{} with {:?}", fix.start, fix.end, fix.replacement); + } + } + Ok(()) +} +``` + +```text +SQC001 47..48 Comparison with NULL is never true + fix: replace 47..48 with "IS" +SQC004 54..59 Row selection is nondeterministic +``` + +## Adding the dependency + +The crate is not published to crates.io. Depend on it from the +[GitHub repository](https://github.com/sqlstack-io/sqlclean), pinned to a release tag or commit: + +```toml +[dependencies] +sqlclean = { git = "https://github.com/sqlstack-io/sqlclean", tag = "" } +``` + +## Entry points + +| Function | What it does | +|----------|--------------| +| `sql_lint::main::lint::lint` | Lints one SQL text with a typed `LintRequest` | +| `sql_lint::main::engine::lint_json` | The same, with the request and response as JSON strings | +| `sql_lint::main::batch_engine::lint_batch_json` | Lints a JSON array of requests in parallel; returns an array of `{"Ok": response}` or `{"Err": message}` | +| `sql_lint::main::formatter::format_json` | Formats SQL from a JSON request | +| `sql_lint::main::catalogue::catalogue` | Every built-in Rule's code, message and remediation | +| `sql_lint::main::default_rules::default_rules` | The codes of the Rules enabled by default | +| `sql_facts::main::file_facts::file_facts` | The statement facts of a SQL text, as custom Rules read them | + +`lint`, `lint_json` and `file_facts` return `Err` with a message when the SQL does not parse under +the dialect. Offsets in requests, findings, edits and facts count Unicode characters, not bytes. + +## Lint requests + +`version` must be `LINT_API_VERSION`; another value is rejected with +`unsupported native lint request version 2; expected 1`. + +| Field | Default | Description | +|-------|---------|-------------| +| `version` | required | `LINT_API_VERSION` | +| `sql` | required | The SQL text | +| `dialect` | required | A [dialect name](configuration.md#dialects) | +| `enabled_rules` | the default Rules | Rule codes or prefixes to run | +| `ignored_rules` | `[]` | Rule codes or prefixes to skip | +| `max_literal_length` | `100` | The `SQC044` limit | +| `max_ranking_order_by` | `5` | The `SQC043` limit | +| `config_file` | `sqlclean.toml` | The configuration file name that remediation text refers to | +| `externally_referenced_ctes` | `[]` | CTE names that something outside the SQL reads, so `SQC005` does not report them | +| `relation_keys` | `{}` | Declared unique keys by relation name; an empty key means the relation holds at most one row, so `SQC038` does not report a cross join with it | + +Other fields describe template code the host has replaced before linting. See `LintRequest` in +`sql_lint::models`. + +A selector that matches no Rule is an error: `unknown native lint rule 'SQC9'`. + +## Fixes + +`LintDiagnostic.fix` holds at most one edit: `start`, `end` and `replacement`. The engine does not +say whether a fix is safe. The `sqlclean` command treats the `SQC001` fix as unsafe, because +turning `= NULL` into `IS NULL` changes which rows match, and every other built-in fix as safe. A +host that applies fixes makes the same decision. See +[Safe and unsafe fixes](rules/overview.md#safe-and-unsafe-fixes). + +## Formatting + +```rust +let request = r#"{"version": 1, "sql": "select order_id, amount from orders where amount > 0", "dialect": "duckdb", "line_width": 100}"#; +println!("{}", format_json(request)?); +``` + +```json +{"version":1,"sql":"SELECT\n order_id,\n amount\nFROM orders\nWHERE amount > 0","changed":true,"formatted":true} +``` + +`indent` sets the spaces per indent step. Without it the engine indents two spaces, its layout +before the `sqlclean` command chose four; pass `"indent": 4` to match `sqlclean format`. + +When the formatter cannot format the SQL, `formatted` is `false`, `sql` is the input unchanged, +and `reason` says why. + +`format_json` formats the text it is given as one unit. `sqlclean format` formats each statement on +its own, joins them with one blank line and ends the file with a newline. To produce the same +layout, split the file with `file_facts`, whose statements carry `start` and `terminated_end`, and +format each statement. See [Formatting](formatting.md#statements). + +## Statement facts + +```rust +let facts = file_facts("SELECT o.order_id FROM orders AS o", "duckdb")?; +for statement in &facts.statements { + for table in &statement.tables { + println!("{} {} {}..{}", statement.kind, table.qualified_name, table.start, table.end); + } +} +``` + +```text +select orders 23..29 +``` + +The fields are those of [Statement facts](custom-rules/statement-facts.md), without `line`, +`column` and `text`. Functions are `NamedNode` values with a `name`. + +## What stays in the host + +The crate reads no files, no configuration and no cache, and does not run Python. A host owns: + +- which files to lint and in which dialect; +- Rule selection, which it passes as `enabled_rules` and `ignored_rules`; +- exceptions, ignores and their reasons; +- the codes and configuration it shows its users; +- caching, and applying fixes. + +The [`sqlclean` command](cli/check.md) is one such host. Its behaviour, described in the rest of +these docs, is a reference for what each of these pieces does. diff --git a/crates/sqlclean-cli/skill/references/docs/formatting.md b/crates/sqlclean-cli/skill/references/docs/formatting.md new file mode 100644 index 0000000..c6d51b1 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/formatting.md @@ -0,0 +1,200 @@ + + +# Formatting + +> One canonical layout for every SQL file, the line width and indent, and files that can't be formatted. + +Online: https://sqlclean.io/docs/formatting/ + +## Contents + +- Before and after +- The layout +- Statements +- Line width +- Indent +- Idempotency +- Formatting in CI +- When a file can't be formatted + +`sqlclean format` prints every statement in one canonical layout. The only style options are the +line width and the indent, so every file in a project looks the same whoever wrote it. +[`sqlclean fix`](cli/fix.md) formats the files it reads too. + +## Before and after + +```sql +-- Revenue per customer +with paid as (select order_id, customer_id, amount from payments where status = 'paid' and amount > 0), +totals as (select customer_id, sum(amount) as revenue, count(*) as orders from paid group by customer_id) +select c.customer_id, c.customer_name, t.revenue, t.orders, case when t.revenue > 1000 then 'gold' when t.revenue > 100 then 'silver' else 'bronze' end as tier +from customers c left join totals t on c.customer_id = t.customer_id +order by t.revenue desc; +delete from orders where status='cancelled';select 1 +``` + +```bash +sqlclean format +``` + +```sql +-- Revenue per customer +WITH paid AS ( + SELECT + order_id, + customer_id, + amount + FROM payments + WHERE + status = 'paid' AND amount > 0 +), + +totals AS ( + SELECT + customer_id, + SUM(amount) AS revenue, + COUNT(*) AS orders + FROM paid + GROUP BY + customer_id +) + +SELECT + c.customer_id, + c.customer_name, + t.revenue, + t.orders, + CASE + WHEN t.revenue > 1000 + THEN 'gold' + WHEN t.revenue > 100 + THEN 'silver' + ELSE 'bronze' + END AS tier +FROM customers c +LEFT JOIN totals t + ON c.customer_id = t.customer_id +ORDER BY + t.revenue DESC; + +DELETE FROM orders WHERE status = 'cancelled'; + +SELECT 1 +``` + +## The layout + +- Clause keywords and operators such as `SELECT`, `LEFT JOIN`, `AND` and `DESC`, and function + names such as `SUM` and `COUNT`, are upper case. Identifiers, aliases and literals keep the case + they were written in. +- Each clause starts a line. Each indent step is four spaces; see [Indent](#indent). +- A `SELECT` list with two or more items puts one item per line. A single item stays on the + `SELECT` line: `SELECT order_id`, `SELECT *`. +- `GROUP BY` and `ORDER BY` items go on the lines below the clause. +- A `WHERE` with one condition stays on the clause line: `WHERE amount > 0`. Several conditions + go on an indented line below it. +- A join's `ON` condition goes on the next line, indented. +- Each CTE closes with `),` and one blank line. One blank line separates the last CTE from the + query that reads it. +- `CASE` puts each `WHEN`, `THEN` and `ELSE` on its own line. +- Comments are kept. + +## Statements + +Each statement in a file is formatted on its own. Statements are separated by one blank line, and +every file ends with a newline. Semicolons are kept where they are written; the formatter does not +add one after the last statement. + +## Line width + +`line_width` sets the width the formatter fills before it breaks a line. The default is `100`: + +```toml +[format] +line_width = 100 +``` + +A line that does not fit is broken at its outermost structure. Inner parentheses that fit on +their line stay on one line. At the default width this `IN` list fits; at `line_width = 60` it is +broken: + +```sql +SELECT order_id +FROM orders +WHERE status IN ('placed', 'paid', 'packed', 'shipped', 'delivered', 'returned') +``` + +```sql +SELECT order_id +FROM orders +WHERE + status IN ( + 'placed', + 'paid', + 'packed', + 'shipped', + 'delivered', + 'returned' + ) +``` + +`line_width` must be positive. + +## Indent + +`indent` sets the number of spaces per indent step. The default is `4`: + +```toml +[format] +indent = 4 +``` + +With `indent = 2`, `select order_id, amount from orders where amount > 0` formats as: + +```sql +SELECT + order_id, + amount +FROM orders +WHERE amount > 0 +``` + +`indent` must be between `1` and `8`. + +## Idempotency + +Formatting is idempotent: a formatted file formats to itself. Running `sqlclean format` twice +changes nothing the second time, so `sqlclean format --check` passes on any file +`sqlclean format` wrote: + +```text +0 file(s) reformatted, 1 file(s) already formatted. +``` + +## Formatting in CI + +`sqlclean format --check` exits with `1` and names every file that would change, without writing: + +```text +Would reformat: models/open_orders.sql +1 file(s) would be reformatted, 0 file(s) already formatted. +``` + +`--diff` prints the changes instead. See [`sqlclean format`](cli/format.md) and +[Integrations](integrations.md). + +## When a file can't be formatted + +The formatter leaves a file unchanged when it cannot format it, usually because the file does not +parse under its [dialect](configuration.md#dialects). Template code such as `{{ col }}` does +not parse: + +```text +j.sql: could not format: Parse error at line 1, column 16: Expected Colon, got RBrace ('}') near [{{ col }} from orders] +``` + +`sqlclean format` and `sqlclean fix` report the file on stderr and exit with `1`, and format every +other file as usual. Fix the SQL, set the file's dialect with +[`[[dialects]]`](configuration.md#dialects), or +[`exclude`](configuration.md#choosing-files) the file. `sqlclean check` reports the same file as +[`SQC999`](rules/overview.md#parse-errors-sqc999). diff --git a/crates/sqlclean-cli/skill/references/docs/index.md b/crates/sqlclean-cli/skill/references/docs/index.md new file mode 100644 index 0000000..42d7053 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/index.md @@ -0,0 +1,83 @@ + + +# Introduction + +> Clean SQL, every time. Fast linting, safe fixes and one canonical format. + +Online: https://sqlclean.io/docs/ + +SQLClean is a free, open-source SQL linter and formatter. It checks `.sql` files against built-in +Rules, applies their fixes, and formats every file in one canonical layout. The engine and the +`sqlclean` command are written in Rust. Custom Rules are plain Python functions that read +structural facts about each statement. + +```bash +sqlclean check +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:1:77 + | + 1 | with open_orders as (select order_id, amount from orders where cancelled_at = null), + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC005]: CTE is unreachable from the final query + --> models/open_orders.sql:2:1 + | + 2 | refunds as (select order_id from payments where amount < 0) + | ^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` + +Found 2 problems (1 fixable with `sqlclean fix`, 1 more with `--unsafe-fixes`). +``` + +`sqlclean fix` removes the unused CTE, then formats the file. The `= NULL` comparison has a fix +too, but it changes which rows match, so it waits for `--unsafe-fixes`. + +## Check + +[`sqlclean check`](cli/check.md) reports findings with the source line, the problem and what to +do about it. Output is text for people, or [JSON and SARIF](integrations.md) for CI and code +scanning. The [built-in Rules](rules/reference.md) cover SQL that runs but returns the wrong +rows: `NULL` comparisons, implicit cartesian joins, `LIMIT` without `ORDER BY`, unused CTEs and +mismatched set operations. Opt-in Rules enforce a consistent query structure. + +## Fix + +[`sqlclean fix`](cli/fix.md) applies every safe fix until the files stop changing, then formats +them. A fix that can change query results is marked unsafe and runs only with `--unsafe-fixes`. +See [Safe and unsafe fixes](rules/overview.md#safe-and-unsafe-fixes). + +## Format + +[`sqlclean format`](cli/format.md) rewrites every file in one canonical layout. Keywords are +upper case, each clause starts a line, and statements are separated by one blank line. Formatting +is idempotent: a formatted file formats to itself. See [Formatting](formatting.md). + +## Extend + +[Custom Rules](custom-rules/overview.md) are Python functions decorated with `@rule`. They read +the tables, joins, CTEs, columns and literals of each statement, can carry fixes and typed options, +and run with the same selection, exceptions and cache as the built-in Rules. Tools that compile or +template SQL can [embed the engine](embedding.md) from Rust. + +## Commands + +| Command | What it does | +|---------|--------------| +| [`sqlclean check`](cli/check.md) | Reports Rule findings without changing any file | +| [`sqlclean fix`](cli/fix.md) | Applies Rule fixes until stable, then formats | +| [`sqlclean format`](cli/format.md) | Rewrites SQL files in the canonical layout | +| [`sqlclean rules`](cli/rules.md) | Lists the available Rules, or shows one | +| [`sqlclean facts`](cli/facts.md) | Prints the statement facts custom Rules read, as JSON | + +## Where to start + + Install SQLClean, then check, fix and format your first file. + Selection, defaults, exceptions and fixes. + Encode your team's SQL conventions in Python. + Every command and flag. diff --git a/crates/sqlclean-cli/skill/references/docs/integrations.md b/crates/sqlclean-cli/skill/references/docs/integrations.md new file mode 100644 index 0000000..9b7ae55 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/integrations.md @@ -0,0 +1,116 @@ + + +# Integrations + +> Run SQLClean in CI with JSON or SARIF output, in pre-commit, and from editors. + +Online: https://sqlclean.io/docs/integrations/ + +## Contents + +- CI +- pre-commit +- Editors + +`sqlclean check`, `fix` and `format` exit with `0` when there is nothing to report, `1` when there +are findings or files to change, and `2` when they cannot run. Any CI system, Git hook or editor +can gate on that. + +## CI + +Run `sqlclean check` and `sqlclean format --check` on every change: + +```bash +sqlclean check +sqlclean format --check +``` + +Both read every matching file under the configuration root. `check` fails on findings and +[stale exceptions](rules/overview.md#stale-exceptions); `format --check` fails on files that +need formatting or can't be formatted. + +### Code scanning with SARIF + +`--output-format sarif` writes a SARIF 2.1.0 log that code-scanning tools show as annotations on +the changed lines. On GitHub Actions: + +```yaml +name: sqlclean + +on: [push, pull_request] + +jobs: + sqlclean: + runs-on: ubuntu-latest + permissions: + contents: read + security-events: write + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - run: pip install sqlclean + - run: sqlclean format --check + - run: sqlclean check --output-format sarif > sqlclean.sarif + - uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: sqlclean.sarif +``` + +`sqlclean check` exits with `1` when it reports findings, so the upload step runs with +`if: always()`. The SARIF log lists findings only; stale exceptions still fail the step. See +[SARIF output](cli/check.md#sarif-output). + +### JSON for other tools + +`--output-format json` prints one document with every finding, its position, message, remediation +and fix safety, the stale exceptions, and a summary. See +[JSON output](cli/check.md#json-output). `sqlclean rules --json` describes every Rule a finding +can come from. + +### Caching in CI + +`check` stores results in `.sqlclean/` under the configuration root. Keep that directory between +runs to skip files that have not changed, or pass `--no-cache` to evaluate everything. See +[Caching](rules/overview.md#caching). + +## pre-commit + +SQLClean ships three [pre-commit](https://pre-commit.com/) hooks: + +| Hook | Runs | Changes files | +|------|------|---------------| +| `sqlclean-check` | `sqlclean check` | No | +| `sqlclean-fix` | `sqlclean fix` | Yes: safe fixes, then formatting | +| `sqlclean-format` | `sqlclean format` | Yes: formatting | + +```yaml +# .pre-commit-config.yaml +repos: + - repo: https://github.com/sqlstack-io/sqlclean + rev: v0.0.1 + hooks: + - id: sqlclean-fix + - id: sqlclean-format +``` + +Use `sqlclean-check` instead of `sqlclean-fix` where a hook must not change files. + +The hooks run on staged `.sql` files and pass them to the command by name, so `exclude` in +`sqlclean.toml` still applies but `include` does not. They use the nearest `sqlclean.toml` at +or above the repository root. A hook that changes a file, or leaves a finding, fails the commit; +stage the changes and commit again. + +## Editors + +Editors can run `sqlclean format` on save, passing the saved file: + +```bash +sqlclean format path/to/file.sql +``` + +The file is formatted with the dialect, `line_width` and `indent` of the nearest `sqlclean.toml` at or above +the editor's working directory. Use `sqlclean check --output-format json path/to/file.sql` to show +findings with their lines and columns. Lines and columns start at 1 and count characters. diff --git a/crates/sqlclean-cli/skill/references/docs/quickstart.md b/crates/sqlclean-cli/skill/references/docs/quickstart.md new file mode 100644 index 0000000..a0714e2 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/quickstart.md @@ -0,0 +1,223 @@ + + +# Quickstart + +> Install SQLClean, then check, fix and format your first SQL file. + +Online: https://sqlclean.io/docs/quickstart/ + +## Contents + +- Prerequisites +- 1. Check a file +- 2. Fix +- 3. Format +- 4. Add a sqlclean.toml +- Next steps + +This guide installs SQLClean, checks one SQL file, fixes what can be fixed, and adds a +`sqlclean.toml` that selects more Rules. + +## Prerequisites + +- Python 3.12 or newer +- SQLClean installed from [PyPI](https://pypi.org/project/sqlclean/): + +```bash +uv tool install sqlclean # or: pip install sqlclean +sqlclean --version +``` + +The `sqlclean` package installs the native `sqlclean` executable. Custom Rules run in the same +Python environment as that executable. + +## 1. Check a file + +Save this query as `models/open_orders.sql`: + +```sql +with open_orders as (select order_id, customer_id, amount from orders where cancelled_at = null), +refunds as (select order_id from payments where amount < 0) +select order_id, customer_id, amount from open_orders limit 100 +``` + +Run `sqlclean check` in the project directory. With no paths, it reads every `.sql` file under the +directory: + +```bash +sqlclean check +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:1:90 + | + 1 | with open_orders as (select order_id, customer_id, amount from orders where cancelled_at = null), + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC005]: CTE is unreachable from the final query + --> models/open_orders.sql:2:1 + | + 2 | refunds as (select order_id from payments where amount < 0) + | ^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` + +error[SQC004]: Row selection is nondeterministic + --> models/open_orders.sql:3:55 + | + 3 | select order_id, customer_id, amount from open_orders limit 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 3 problems (1 fixable with `sqlclean fix`, 1 more with `--unsafe-fixes`). +``` + +Each finding names its Rule code, the file, line and column, and what to do. The command exits +with `1` because there are findings. + +## 2. Fix + +Preview the safe fixes as a diff, then apply them: + +```bash +sqlclean fix --diff +sqlclean fix +``` + +```text +Fixed 1 problem(s) and changed 1 file(s). +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:7:24 + | + 7 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` + +error[SQC004]: Row selection is nondeterministic + --> models/open_orders.sql:15:1 + | + 15 | LIMIT 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 2 problems (1 fixable with `sqlclean fix --unsafe-fixes`). +``` + +`fix` removed the unused `refunds` CTE and formatted the file. The `= NULL` fix turns the +comparison into `IS NULL`, which changes which rows match, so it is unsafe. Review it, then apply +it: + +```bash +sqlclean fix --unsafe-fixes +``` + +`SQC004` has no fix: only you know which column makes the order deterministic. Add +`ORDER BY order_id` before `LIMIT 100`, then run `sqlclean fix` once more to format the new +clause. The file is now: + +```sql +WITH open_orders AS ( + SELECT + order_id, + customer_id, + amount + FROM orders + WHERE cancelled_at IS NULL +) + +SELECT + order_id, + customer_id, + amount +FROM open_orders +ORDER BY + order_id +LIMIT 100 +``` + +```bash +sqlclean check +``` + +```text +All checks passed. +``` + +## 3. Format + +`sqlclean fix` formats the files it reads. To format without applying fixes, or to check +formatting in CI, use `sqlclean format`: + +```bash +sqlclean format # rewrite files +sqlclean format --check # exit 1 when a file needs formatting +``` + +See [Formatting](formatting.md) for the layout. + +## 4. Add a sqlclean.toml + +Without a configuration file, SQLClean parses files as `generic` SQL and runs the 13 Rules that +are enabled by default. Create `sqlclean.toml` in the project root to set the dialect and select +every built-in Rule: + +```toml +dialect = "duckdb" + +[rules] +select = ["SQC"] +``` + +`SQC` is a prefix, so it selects all 45 built-in Rules. Three of the opt-in structure Rules now +report the file: + +```text +error[SQC041]: Only the last top-level CTE may be named final, and it must use that name + --> models/open_orders.sql:1:6 + | + 1 | WITH open_orders AS ( + | ^^^^^^^^^^^ + = help: Name the last top-level CTE final and rename any other final CTE; SQC035 checks the terminal SELECT. + +error[SQC034]: Query keeps transformation logic outside top-level CTEs + --> models/open_orders.sql:10:1 + | + 10 | SELECT + | ^^^^^^ + = help: Move transformation logic into named top-level CTEs before the terminal SELECT. + +error[SQC035]: Terminal SELECT does not read plainly from the final top-level CTE + --> models/open_orders.sql:10:1 + | + 10 | SELECT + | ^^^^^^ + = help: Name the final logical CTE and leave one terminal SELECT from that CTE. + +Found 3 problems. +``` + +Follow them, or skip the ones your team does not want: + +```toml +dialect = "duckdb" + +[rules] +select = ["SQC"] +ignore = ["SQC034", "SQC035", "SQC041"] +``` + +```text +All checks passed. +``` + +## Next steps + +- [Configuration](configuration.md) lists every key, including per-path dialects. +- [Rules](rules/overview.md) covers selection, exceptions and fixes, and the + [Rule reference](rules/reference.md) lists every built-in Rule. +- [Integrations](integrations.md) runs SQLClean in CI and pre-commit. +- [Custom Rules](custom-rules/overview.md) checks your own conventions. diff --git a/crates/sqlclean-cli/skill/references/docs/rules/overview.md b/crates/sqlclean-cli/skill/references/docs/rules/overview.md new file mode 100644 index 0000000..5c13b1b --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/rules/overview.md @@ -0,0 +1,273 @@ + + +# Overview + +> Select Rules, record reviewed exceptions, and apply safe and unsafe fixes. + +Online: https://sqlclean.io/docs/rules/overview/ + +## Contents + +- Selecting Rules +- Default Rules +- Thresholds +- Exceptions +- Path ignores +- Parse errors (SQC999) +- Safe and unsafe fixes +- Caching +- Continue + +A Rule is one check over SQL. It reports a finding at an exact position, with a message, a +remediation, and sometimes a fix. SQLClean has 45 built-in Rules with `SQC###` codes, and runs +[custom Rules](../custom-rules/overview.md) written in Python with `XSQC` codes. + +Consider a query that returns an arbitrary hundred rows: + +```sql +SELECT order_id +FROM orders +LIMIT 100 +``` + +```bash +sqlclean check +``` + +```text +error[SQC004]: Row selection is nondeterministic + --> models/recent_orders.sql:3:1 + | + 3 | LIMIT 100 + | ^^^^^ + = help: Add ORDER BY with a deterministic tie-breaker before LIMIT or OFFSET. + +Found 1 problem. +``` + +Add an ordering and check again: + +```sql +SELECT order_id +FROM orders +ORDER BY order_id +LIMIT 100 +``` + +Run `sqlclean rules` for every Rule, or see the [Rule reference](reference.md). + +## Selecting Rules + +Select Rules in `sqlclean.toml` by exact code or by prefix: + +```toml +[rules] +select = ["SQC"] # every built-in Rule +ignore = ["SQC027", "SQC03"] # except SQC027 and SQC030 to SQC039 +``` + +- An exact code such as `SQC004` selects one Rule. +- A prefix selects every Rule whose code starts with it: `SQC` selects all built-in Rules, + `SQC00` selects `SQC001` to `SQC009`, and `XSQCNAME` selects every custom Rule in the `NAME` + family. +- `ignore` removes Rules from the selection, by code or prefix. +- A selector that matches no Rule is an error: `Rule selector 'SQC9' matches no Rule`. + +On the command line, `--select` replaces `[rules] select` for one run, and `--ignore` adds to +`[rules] ignore`: + +```bash +sqlclean check --select SQC001,SQC004 +sqlclean check --ignore SQC039 +``` + +`select = []` runs no Rules. Files are still parsed, so [parse errors](#parse-errors-sqc999) are +still reported. + +## Default Rules + +Without `[rules] select`, SQLClean runs the 13 Rules that are enabled by default. They cover +mistakes that run without an error and are easy to miss in review: NULL handling, joins, row order, +duplicate names and unused CTEs. + +| Code | Finding | +|------|---------| +| `SQC001` | Comparison with NULL is never true | +| `SQC002` | Comma-separated sources create an implicit cartesian join | +| `SQC003` | Non-cross join has no meaningful condition | +| `SQC004` | Row selection is nondeterministic | +| `SQC005` | CTE is unreachable from the final query | +| `SQC006` | DISTINCT is redundant with the grouped output | +| `SQC007` | Positional set operation is vulnerable to column-order drift | +| `SQC009` | Table alias is duplicated in the same query scope | +| `SQC010` | Output alias is duplicated in the same select list | +| `SQC018` | ROW_NUMBER has no deterministic ordering | +| `SQC019` | NOT IN list contains NULL and can reject every row | +| `SQC020` | Set-operation branches project different column counts | +| `SQC039` | Inline query relation obscures data flow | + +The other 32 Rules run only when `select` names them, by code or prefix. A prefix selects every +matching Rule, whether it is enabled by default or not. A custom Rule is enabled by default when its +`@rule` sets `enabled_by_default=True`. + +## Thresholds + +Two built-in Rules compare against a limit, set under `[rules.thresholds]`: + +```toml +[rules.thresholds] +max_literal_length = 100 # SQC044: the longest string literal allowed +max_ranking_order_by = 5 # SQC043: the most ORDER BY expressions in a ranking window +``` + +Both default to the values shown and must be positive. Built-in Rules take no other options. + +## Exceptions + +An exception allows one reviewed finding: one Rule code in one file. It lives in `sqlclean.toml`, +so a reviewer sees it in the diff, and it needs a reason: + +```toml +[[rules.rule_exceptions]] +rule = "SQC004" +path = "models/order_preview.sql" +reason = "A preview of any hundred orders is enough." +``` + +`rule` is an exact code, not a prefix. `path` is the file's path relative to the configuration +root, as findings print it. The exception covers every finding of that Rule in that file. + +SQLClean has no inline suppression comments. A comment such as `-- noqa: SQC001` changes nothing; +record the exception in `sqlclean.toml` instead. + +### Cross join exceptions + +A file-wide exception for a deliberate `CROSS JOIN` would also hide any accidental cross join added +to the file later. `SQC038` findings name the joined relation and, inside a CTE, that CTE, so an +exception can cover exactly one join: + +```text +error[SQC038]: CROSS JOIN with `regions` in CTE `pairs` creates an explicit cartesian product + --> models/calendar.sql:11:3 +``` + +```toml +[[rules.rule_exceptions]] +rule = "SQC038" +path = "models/calendar.sql" +relation = "regions" +cte = "pairs" # optional: only the join inside this CTE +reason = "Every order date is reported for every region." +``` + +`relation` and `cte` compare without case. Without `cte`, the exception covers joins with that +relation anywhere in the file. `cte` needs `relation`, and `relation` matches only `SQC038` +findings. + +`SQC038` does not report a cross join when one side is provably at most one row, such as a CTE +that aggregates without `GROUP BY`, `LIMIT 1` or a one-row `VALUES`, so many intentional cross +joins need no exception: + +```sql +WITH latest AS ( + SELECT MAX(order_date) AS latest_date + FROM orders +) + +SELECT + o.order_id, + l.latest_date +FROM orders AS o +CROSS JOIN latest AS l +``` + +### Stale exceptions + +An exception that matches no finding is stale. `check` and `fix` report it and exit with `1`: + +```text +error: stale rule exception: SQC004 in models/orders.sql matches no finding; remove it from sqlclean.toml +``` + +Delete the entry. Exceptions are only judged stale when their file was checked and their Rule was +selected, so `sqlclean check models/customers.sql` or `--select SQC001` never reports an exception +for another file or Rule. + +## Path ignores + +A path ignore turns Rules off for files that follow a different convention. It takes codes or +prefixes, glob patterns, and a reason: + +```toml +[[rules.rule_ignores]] +rules = ["SQC034", "SQC035"] +paths = ["scratch/**"] +reason = "Exploratory queries." +``` + +Patterns are relative to the configuration root, as in [`include`](../configuration.md#choosing-files). +To turn a Rule off everywhere, add it to `[rules] ignore` instead. + +## Parse errors (SQC999) + +A file that does not parse under its [dialect](../configuration.md#dialects) gets one `SQC999` +finding and no other: + +```text +error[SQC999]: SQL could not be parsed as duckdb: Parse error at line 1, column 18: Expected table name or subquery, got Where + --> scratch/broken.sql:1:1 + | + 1 | SELECT FROM WHERE orders + | ^ + = help: Fix the syntax error, or set this file's dialect in sqlclean.toml. +``` + +The finding points at the start of the file; the message gives the line and column of the error. +`SQC999` is not a selectable Rule: selection, `ignore`, path ignores and exceptions never hide it. +Fix the SQL, set the right dialect for the file, or [`exclude`](../configuration.md#choosing-files) +the file. + +## Safe and unsafe fixes + +Some Rules carry a fix. [`sqlclean fix`](../cli/fix.md) applies it. + +- A **safe** fix keeps the query's results. `SQC005` removes an unused CTE; `SQC002` turns + `FROM orders, customers` into `FROM orders CROSS JOIN customers`. +- An **unsafe** fix can change results. `SQC001` turns `cancelled_at = NULL`, which matches no row, + into `cancelled_at IS NULL`, which matches rows. That is almost always the intent, but it is a + different query. Unsafe fixes apply only with `--unsafe-fixes`. + +`sqlclean check` says which command applies each fix: + +```text + = fix: available with `sqlclean fix --unsafe-fixes` +``` + +The [Rule reference](reference.md) lists which Rules can fix and how safely. + +## Caching + +`check` and `fix` reuse the results of earlier runs for inputs that have not changed, so a second +run over an unchanged project costs little more than reading the files. Results are stored in +`.sqlclean/` under the configuration root, which has its own `.gitignore`. + +A file's results are evaluated again when any of these change: + +- the file's text, path or dialect; +- the selected Rules or `[rules.thresholds]`; +- a custom Rule's options, or any Python file under `[rules] custom`; +- for a custom Rule over the whole repository, any checked file; +- the SQLClean version. + +Exceptions and ignores apply after the cache, so editing them takes effect at once. + +Pass `--no-cache` to evaluate every file without reading or writing the cache. Use it when a custom +Rule imports Python from outside its `[rules] custom` paths, since changes there are not tracked. +`sqlclean format` does not use the cache. + +## Continue + + Every built-in Rule, its default and its fix. + Define your own Rules in Python. + Every `sqlclean.toml` key. + Flags, output formats and exit codes. diff --git a/crates/sqlclean-cli/skill/references/docs/rules/reference.md b/crates/sqlclean-cli/skill/references/docs/rules/reference.md new file mode 100644 index 0000000..eff1381 --- /dev/null +++ b/crates/sqlclean-cli/skill/references/docs/rules/reference.md @@ -0,0 +1,275 @@ + + +# Rule reference + +> Every built-in Rule, whether it runs by default, and whether it has a fix. + +Online: https://sqlclean.io/docs/rules/reference/ + +## Contents + +- SQC001: NULL comparisons +- SQC002 and SQC003: joins without keys +- SQC005: unused CTEs +- SQC018: unstable row numbers +- SQC019: NULL in NOT IN +- SQC039: inline subqueries +- SQC034, SQC035 and SQC041: query structure + +SQLClean has 45 built-in Rules. `Default` Rules run when `[rules] select` is not set; the others +run when `select` names them by code or prefix. A Rule with a `Safe` fix is fixed by +`sqlclean fix`; an `Unsafe` fix also needs `--unsafe-fixes`. See +[Selecting Rules](overview.md#selecting-rules) and +[Safe and unsafe fixes](overview.md#safe-and-unsafe-fixes). + +Run `sqlclean rules ` for a Rule's remediation. + +| Code | Finding | Default | Fix | +|------|---------|---------|-----| +| `SQC001` | Comparison with NULL is never true | Yes | Unsafe | +| `SQC002` | Comma-separated sources create an implicit cartesian join | Yes | Safe | +| `SQC003` | Non-cross join has no meaningful condition | Yes | Safe | +| `SQC004` | Row selection is nondeterministic | Yes | — | +| `SQC005` | CTE is unreachable from the final query | Yes | Safe | +| `SQC006` | DISTINCT is redundant with the grouped output | Yes | Safe | +| `SQC007` | Positional set operation is vulnerable to column-order drift | Yes | — | +| `SQC008` | Bare UNION leaves duplicate handling implicit | No | Safe | +| `SQC009` | Table alias is duplicated in the same query scope | Yes | — | +| `SQC010` | Output alias is duplicated in the same select list | Yes | — | +| `SQC011` | Clause mixes positional and named references | No | — | +| `SQC012` | ELSE NULL is redundant | No | — | +| `SQC013` | DISTINCT is written as though it were a function | No | Safe | +| `SQC014` | Predicate compares identical constant values | No | — | +| `SQC015` | Empty statement follows another terminator | No | — | +| `SQC016` | Expression is aliased to its existing name | No | — | +| `SQC017` | Row count uses a constant expression | No | Safe | +| `SQC018` | ROW_NUMBER has no deterministic ordering | Yes | — | +| `SQC019` | NOT IN list contains NULL and can reject every row | Yes | — | +| `SQC020` | Set-operation branches project different column counts | Yes | — | +| `SQC021` | Query output has an uncontrolled column shape | No | — | +| `SQC022` | Calculated projection has no stable output name | No | — | +| `SQC023` | Table alias is never referenced | No | — | +| `SQC024` | WHERE predicate rejects NULL rows from a LEFT JOIN | No | — | +| `SQC025` | JOIN type is implicit | No | Safe | +| `SQC026` | ORDER BY mixes explicit and implicit directions | No | — | +| `SQC027` | Column reference is unqualified in a multi-relation query | No | — | +| `SQC028` | Single-relation query mixes qualified and unqualified columns | No | — | +| `SQC029` | Qualified reference uses a relation absent from this query scope | No | — | +| `SQC030` | CASE expression only converts a condition to TRUE or FALSE | No | Safe | +| `SQC031` | CASE expression is nested directly inside ELSE | No | — | +| `SQC032` | Joined relation contributes no referenced values | No | — | +| `SQC033` | Comment is not attached to the syntax node it explains | No | — | +| `SQC034` | Query keeps transformation logic outside top-level CTEs | No | — | +| `SQC035` | Terminal SELECT does not read plainly from the final top-level CTE | No | — | +| `SQC036` | CTE is nested inside another query | No | — | +| `SQC037` | Recursive CTE is not permitted | No | — | +| `SQC038` | CROSS JOIN creates an explicit cartesian product | No | — | +| `SQC039` | Inline query relation obscures data flow | Yes | — | +| `SQC040` | JOIN ON contains a computed expression | No | — | +| `SQC041` | Only the last top-level CTE may be named final, and it must use that name | No | — | +| `SQC042` | CTE output column is never read later in the query | No | — | +| `SQC043` | Ranking window sorts by more expressions than max_ranking_order_by | No | — | +| `SQC044` | String literal is longer than max_literal_length | No | — | +| `SQC045` | JSON payload is parsed more than once in one SELECT | No | — | + +`SQC999` is the [parse error](overview.md#parse-errors-sqc999) finding. It is not a +selectable Rule. + +`SQC043` and `SQC044` read their limits from +[`[rules.thresholds]`](overview.md#thresholds). + +## SQC001: NULL comparisons + +`= NULL` and `<> NULL` are never true, so the filter matches no row: + +```sql +SELECT order_id +FROM orders +WHERE cancelled_at = NULL +``` + +```text +error[SQC001]: Comparison with NULL is never true + --> models/open_orders.sql:3:20 + | + 3 | WHERE cancelled_at = NULL + | ^ + = help: Use IS NULL or IS NOT NULL when testing for NULL. + = fix: available with `sqlclean fix --unsafe-fixes` +``` + +The fix writes `WHERE cancelled_at IS NULL`. It is unsafe because the query then returns rows it +did not return before. + +## SQC002 and SQC003: joins without keys + +A comma between sources joins every row with every row unless a `WHERE` predicate filters them: + +```sql +SELECT + o.order_id, + c.customer_name +FROM orders AS o, customers AS c +WHERE o.customer_id = c.customer_id +``` + +The `SQC002` fix keeps the meaning and makes the cartesian product visible: + +```diff +-FROM orders AS o, customers AS c ++FROM orders AS o ++CROSS JOIN customers AS c + WHERE o.customer_id = c.customer_id +``` + +Move the predicate into `JOIN customers AS c ON o.customer_id = c.customer_id` by hand to make it a +keyed join. `SQC003` reports a `JOIN` whose condition is missing or constant, such as +`ON 1 = 1`; its fix turns a `JOIN` without a condition into `CROSS JOIN`. + +## SQC005: unused CTEs + +A CTE that no reachable query reads is dead code: + +```sql +WITH paid AS ( + SELECT order_id FROM payments +), + +refunded AS ( + SELECT order_id FROM refunds +) + +SELECT order_id FROM paid +``` + +```text +error[SQC005]: CTE is unreachable from the final query + --> models/paid_orders.sql:5:1 + | + 5 | refunded AS ( + | ^^^^^^^^ + = help: Reference the CTE from the final query or another reachable CTE, or remove it. + = fix: available with `sqlclean fix` +``` + +The fix removes `refunded`. + +## SQC018: unstable row numbers + +Without `ORDER BY` in the window, the row numbered `1` can change between runs: + +```sql +SELECT + order_id, + ROW_NUMBER() OVER (PARTITION BY customer_id) AS order_rank +FROM orders +``` + +```text +error[SQC018]: ROW_NUMBER has no deterministic ordering +``` + +An `ORDER BY` is not always enough. Ties still rank in any order unless `PARTITION BY` and +`ORDER BY` together cover a unique key of the ranked rows. SQLClean only knows a key it can prove +from the query, such as the `GROUP BY` columns of a CTE. Ranking straight over a table is reported +with a second message: + +```text +error[SQC018]: Ranking window order is not proven unique within its partition + --> models/ranked.sql:3:3 + | + 3 | ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at, order_id) AS order_rank + | ^^^^^^^^^^ + = help: PARTITION BY and ORDER BY must together cover a unique key of the ranked rows. End the ORDER BY with a unique, non-null tie-breaker column. If the rows have no unique column, build one upstream from the columns that identify a row (for example `HASH(source_partition, source_offset, line_index) AS row_key`) and order by it last. +``` + +Here the CTE's `GROUP BY` makes `customer_id, order_id` a key, so the window passes: + +```sql +WITH customer_orders AS ( + SELECT + customer_id, + order_id, + MAX(ordered_at) AS ordered_at + FROM orders + GROUP BY + customer_id, + order_id +) + +SELECT + order_id, + ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY ordered_at, order_id) AS order_rank +FROM customer_orders +``` + +`SQC004` reports the same problem for `LIMIT` and `OFFSET`; see the example in +[Rules](overview.md). + +## SQC019: NULL in NOT IN + +`x NOT IN (..., NULL)` is never true, so the filter rejects every row: + +```sql +SELECT order_id +FROM orders +WHERE status NOT IN ('cancelled', NULL) +``` + +```text +error[SQC019]: NOT IN list contains NULL and can reject every row + --> models/active_orders.sql:3:14 + | + 3 | WHERE status NOT IN ('cancelled', NULL) + | ^^^ + = help: Remove NULL from the list or express the anti-join with a NULL-safe NOT EXISTS. +``` + +## SQC039: inline subqueries + +A subquery in `FROM` hides a step of the query. Name it as a CTE instead: + +```sql +-- Reported +SELECT order_id +FROM (SELECT order_id FROM orders WHERE amount > 0) AS paid + +-- Passes +WITH paid AS ( + SELECT order_id + FROM orders + WHERE amount > 0 +) + +SELECT order_id +FROM paid +``` + +## SQC034, SQC035 and SQC041: query structure + +These opt-in Rules ask every query to do its work in top-level CTEs, name the last one `final`, +and end with a plain `SELECT` of named columns from it. This query passes every built-in Rule: + +```sql +WITH open_orders AS ( + SELECT + order_id, + amount + FROM orders + WHERE cancelled_at IS NULL +), + +final AS ( + SELECT + order_id, + amount + FROM open_orders +) + +SELECT + order_id, + amount +FROM final +``` + +Ending with `SELECT * FROM final` is reported by `SQC021`, which asks for named output columns. diff --git a/crates/sqlclean-cli/src/command/_helpers/dispatch.rs b/crates/sqlclean-cli/src/command/_helpers/dispatch.rs index 1a76862..bdc1a0f 100644 --- a/crates/sqlclean-cli/src/command/_helpers/dispatch.rs +++ b/crates/sqlclean-cli/src/command/_helpers/dispatch.rs @@ -50,6 +50,9 @@ fn execute(cli: Cli) -> Result { Command::Rules(arguments) => { crate::command::_helpers::rules_command::run(&config, &arguments) } + Command::Skills(arguments) => { + crate::command::_helpers::skills_command::run(&config.root, &arguments) + } Command::Facts(_) => Ok(EXIT_SUCCESS), } } diff --git a/crates/sqlclean-cli/src/command/_helpers/mod.rs b/crates/sqlclean-cli/src/command/_helpers/mod.rs index a54f409..8a524c2 100644 --- a/crates/sqlclean-cli/src/command/_helpers/mod.rs +++ b/crates/sqlclean-cli/src/command/_helpers/mod.rs @@ -5,3 +5,4 @@ pub mod facts_command; pub mod fix_command; pub mod format_command; pub mod rules_command; +pub mod skills_command; diff --git a/crates/sqlclean-cli/src/command/_helpers/skills_command.rs b/crates/sqlclean-cli/src/command/_helpers/skills_command.rs new file mode 100644 index 0000000..72dc206 --- /dev/null +++ b/crates/sqlclean-cli/src/command/_helpers/skills_command.rs @@ -0,0 +1,140 @@ +//! `sqlclean skills`: install the bundled SQLClean skill for AI coding agents. + +use std::fs; +use std::io::Write; +use std::path::{Path, PathBuf}; + +use include_dir::{Dir, DirEntry, include_dir}; + +use crate::command::constants::{ + DEFAULT_SKILL_TARGETS, EXIT_SUCCESS, SKILL_FILE_EXTENSION, SKILL_FILE_SUFFIX, + SKILL_GENERATED_MARKER, SKILL_NAME, SKILL_TARGETS, +}; +use crate::command::models::SkillsArgs; + +static SKILL: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/skill"); + +pub(crate) fn run(root: &Path, arguments: &SkillsArgs) -> Result { + let base: PathBuf = if arguments.global { + home()? + } else { + root.to_path_buf() + }; + let targets: Vec<&str> = if arguments.target.is_empty() { + DEFAULT_SKILL_TARGETS.to_vec() + } else { + arguments.target.iter().map(String::as_str).collect() + }; + let mut skill_dirs: Vec = Vec::new(); + for target in targets { + let (_, local, global) = SKILL_TARGETS + .iter() + .find(|(name, _, _)| *name == target) + .ok_or_else(|| { + format!("unknown skill target '{target}'; expected agents, claude or opencode") + })?; + let relative: &str = if arguments.global { global } else { local }; + let skill_dir: PathBuf = base.join(relative).join(SKILL_NAME); + if !skill_dirs.contains(&skill_dir) { + skill_dirs.push(skill_dir); + } + } + let files: Vec<(String, &str)> = packaged_files(&SKILL); + if !arguments.force { + for skill_dir in &skill_dirs { + refuse_unowned(skill_dir, &files)?; + } + } + let mut stdout = std::io::stdout(); + for skill_dir in &skill_dirs { + install(skill_dir, &files)?; + let _ = writeln!(stdout, "Installed {}", skill_dir.join("SKILL.md").display()); + } + Ok(EXIT_SUCCESS) +} + +fn home() -> Result { + std::env::var_os("HOME") + .or_else(|| std::env::var_os("USERPROFILE")) + .map(PathBuf::from) + .ok_or_else(|| "cannot find the home directory for --global".to_owned()) +} + +fn packaged_files(dir: &'static Dir<'static>) -> Vec<(String, &'static str)> { + let mut files: Vec<(String, &str)> = Vec::new(); + for entry in dir.entries() { + match entry { + DirEntry::Dir(child) => files.extend(packaged_files(child)), + DirEntry::File(file) => { + let relative: String = file.path().to_string_lossy().replace('\\', "/"); + if let (true, Some(text)) = + (relative.ends_with(SKILL_FILE_SUFFIX), file.contents_utf8()) + { + files.push((relative, text)); + } + } + } + } + files.sort(); + files +} + +fn generated(path: &Path) -> bool { + fs::read_to_string(path).is_ok_and(|text| text.contains(SKILL_GENERATED_MARKER)) +} + +fn refuse_unowned(skill_dir: &Path, files: &[(String, &str)]) -> Result<(), String> { + for (relative, _) in files { + let path: PathBuf = skill_dir.join(relative); + if path.is_file() && !generated(&path) { + return Err(format!( + "refusing to overwrite a skill file SQLClean did not generate: {}; rerun with --force to replace it", + path.display() + )); + } + } + Ok(()) +} + +/// Write every packaged file, then remove generated files the package no longer ships. +fn install(skill_dir: &Path, files: &[(String, &str)]) -> Result<(), String> { + for (relative, text) in files { + let path: PathBuf = skill_dir.join(relative); + if fs::read_to_string(&path).is_ok_and(|current| current == *text) { + continue; + } + if let Some(parent) = path.parent() { + fs::create_dir_all(parent).map_err(|error| error.to_string())?; + } + fs::write(&path, text).map_err(|error| format!("{}: {error}", path.display()))?; + } + for path in markdown_files(skill_dir) { + let relative: String = path + .strip_prefix(skill_dir) + .map(|relative| relative.to_string_lossy().replace('\\', "/")) + .unwrap_or_default(); + if !files.iter().any(|(shipped, _)| *shipped == relative) && generated(&path) { + fs::remove_file(&path).map_err(|error| error.to_string())?; + if let Some(parent) = path.parent() { + let _ = fs::remove_dir(parent); + } + } + } + Ok(()) +} + +fn markdown_files(dir: &Path) -> Vec { + let mut found: Vec = Vec::new(); + for entry in fs::read_dir(dir).into_iter().flatten().flatten() { + let path: PathBuf = entry.path(); + if path.is_dir() { + found.extend(markdown_files(&path)); + } else if path + .extension() + .is_some_and(|extension| extension == SKILL_FILE_EXTENSION) + { + found.push(path); + } + } + found +} diff --git a/crates/sqlclean-cli/src/command/constants.rs b/crates/sqlclean-cli/src/command/constants.rs index 3c6397c..233e2f8 100644 --- a/crates/sqlclean-cli/src/command/constants.rs +++ b/crates/sqlclean-cli/src/command/constants.rs @@ -3,3 +3,18 @@ pub const EXIT_SUCCESS: u8 = 0; pub const EXIT_FINDINGS: u8 = 1; /// The command could not run: invalid arguments, configuration or input. pub const EXIT_ERROR: u8 = 2; +/// The directory name of the installed skill under each agent's skills directory. +pub(crate) const SKILL_NAME: &str = "sqlclean"; +/// The comment that marks a skill file as written by `sqlclean skills`. +pub(crate) const SKILL_GENERATED_MARKER: &str = ""; +/// Each agent target with its project-relative and home-relative skills directory. +pub(crate) const SKILL_TARGETS: [(&str, &str, &str); 3] = [ + ("agents", ".agents/skills", ".agents/skills"), + ("claude", ".claude/skills", ".claude/skills"), + ("opencode", ".opencode/skills", ".config/opencode/skills"), +]; +/// The targets installed when no `--target` is given; OpenCode also reads `.agents`. +pub(crate) const DEFAULT_SKILL_TARGETS: [&str; 2] = ["agents", "claude"]; +/// Only Markdown files are installed and cleaned up. +pub(crate) const SKILL_FILE_EXTENSION: &str = "md"; +pub(crate) const SKILL_FILE_SUFFIX: &str = ".md"; diff --git a/crates/sqlclean-cli/src/command/models.rs b/crates/sqlclean-cli/src/command/models.rs index 0d04fc5..72ff5f0 100644 --- a/crates/sqlclean-cli/src/command/models.rs +++ b/crates/sqlclean-cli/src/command/models.rs @@ -30,6 +30,8 @@ pub enum Command { Rules(RulesArgs), /// Print the structural facts custom Rules read for SQL on standard input, as JSON. Facts(FactsArgs), + /// Install the SQLClean skill so AI coding agents know its commands and Rules. + Skills(SkillsArgs), } /// Rule selection shared by check and fix. @@ -93,3 +95,16 @@ pub struct FactsArgs { #[arg(long, default_value = "generic")] pub dialect: String, } + +#[derive(Debug, Args)] +pub struct SkillsArgs { + /// Agent to install for: agents, claude or opencode. Repeat for several; default agents and claude. + #[arg(long)] + pub target: Vec, + /// Install under the home directory instead of the configuration root. + #[arg(long)] + pub global: bool, + /// Overwrite skill files that SQLClean did not generate. + #[arg(long)] + pub force: bool, +} diff --git a/fensu.toml b/fensu.toml index f0c562d..7443fe2 100644 --- a/fensu.toml +++ b/fensu.toml @@ -2,7 +2,7 @@ analyzer = "python" roots = ["src/sqlclean"] tests = ["tests"] -tooling = [] +tooling = ["scripts"] select = ["FF"] [[targets.python.rule_exceptions]] @@ -47,3 +47,7 @@ reason = "This entry exists for the sql_lint tests, which the analyzer does not rule = "FPRSL105" path = "crates/sqlclean/src/sql_tokens/main/query_fingerprint.rs" reason = "This test-support entry checks that fixes preserve meaning in the sql_lint tests, which the analyzer does not count as importers." + +[targets.rust.rule_options.FPRSR311] +# `skill/` is the agent skill the `sqlclean skills` command embeds; scripts.generate_docs_skill writes its references. +allowed_entries = ["Cargo.toml", "build.rs", "src/", "tests/", "benches/", "examples/", "README*", "LICENSE*", "skill/"] diff --git a/packages/sqlclean-cli/pyproject.toml b/packages/sqlclean-cli/pyproject.toml index 2b05d5e..8fbd46e 100644 --- a/packages/sqlclean-cli/pyproject.toml +++ b/packages/sqlclean-cli/pyproject.toml @@ -33,4 +33,5 @@ cache-keys = [ { file = "../../Cargo.lock" }, { file = "../../crates/*/Cargo.toml" }, { file = "../../crates/*/src/**/*.rs" }, + { file = "../../crates/sqlclean-cli/skill/**/*.md" }, ] diff --git a/pyproject.toml b/pyproject.toml index 7ab39a3..60ae1a5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,6 +65,8 @@ pythonpath = ["."] [tool.ruff] target-version = "py312" line-length = 100 +# Generated from the website docs by `make skills`; ruff would reformat their Python examples. +extend-exclude = ["crates/sqlclean-cli/skill"] [tool.ruff.lint] select = ["B", "E", "F", "I", "PLR0913", "PLR0915", "UP"] diff --git a/scripts/__init__.py b/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/check_docs_skill.py b/scripts/check_docs_skill.py new file mode 100644 index 0000000..4fbe11f --- /dev/null +++ b/scripts/check_docs_skill.py @@ -0,0 +1,15 @@ +"""Direct wrapper for the docs skill freshness check.""" + +from __future__ import annotations + +from scripts.docs_skill.main.check import check_docs_skill + + +def main(argv: list[str] | None = None) -> int: + """Check that the SQLClean documentation skill is up to date.""" + + return check_docs_skill(argv) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/docs_skill/__init__.py b/scripts/docs_skill/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/docs_skill/_helpers/__init__.py b/scripts/docs_skill/_helpers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/docs_skill/_helpers/mdx.py b/scripts/docs_skill/_helpers/mdx.py new file mode 100644 index 0000000..94c0424 --- /dev/null +++ b/scripts/docs_skill/_helpers/mdx.py @@ -0,0 +1,85 @@ +"""MDX parsing and cleanup for skill generation.""" + +import re + +from scripts.docs_skill.constants import COMPONENT_TAG_END, FRONTMATTER_DELIMITER +from scripts.docs_skill.models import MdxPage + + +def parse_mdx_page(contents: str) -> MdxPage: + """Parse frontmatter and Markdown-compatible content from one MDX page.""" + + frontmatter, body = _split_frontmatter(contents) + return MdxPage( + title=frontmatter.get("title") or "Untitled", + description=frontmatter.get("description", ""), + body=_clean_mdx_body(body), + ) + + +def _split_frontmatter(contents: str) -> tuple[dict[str, str], str]: + lines: list[str] = contents.splitlines() + if not lines or lines[0].strip() != FRONTMATTER_DELIMITER: + return {}, contents + + frontmatter: dict[str, str] = {} + end_index: int = 0 + for index, line in enumerate(lines[1:], start=1): + if line.strip() == FRONTMATTER_DELIMITER: + end_index = index + break + key, separator, value = line.partition(":") + if separator: + frontmatter[key.strip()] = value.strip().strip('"') + + if end_index == 0: + return {}, contents + + return frontmatter, "\n".join(lines[end_index + 1 :]) + + +def _clean_mdx_body(body: str) -> str: + cleaned_lines: list[str] = [] + in_code_block: bool = False + in_component_tag: bool = False + + for raw_line in body.splitlines(): + line: str = raw_line.rstrip() + if line.startswith("```"): + in_code_block = not in_code_block + cleaned_lines.append(line) + continue + if in_code_block: + cleaned_lines.append(line) + continue + + stripped: str = line.strip() + if in_component_tag: + in_component_tag = COMPONENT_TAG_END not in stripped + continue + if re.match(r"<[A-Z][\w.]*\b", stripped) and COMPONENT_TAG_END not in stripped: + in_component_tag = True + continue + if _is_mdx_only_line(stripped): + continue + cleaned_lines.append(line) + + return normalize_blank_lines("\n".join(cleaned_lines)).strip() + + +def _is_mdx_only_line(stripped: str) -> bool: + if not stripped: + return False + if stripped.startswith(("import ", "export ")): + return True + if re.fullmatch(r"]*>", stripped): + return True + if re.fullmatch(r"<[A-Z][^>]*/>", stripped): + return True + return bool(re.fullmatch(r"]*/>", stripped)) + + +def normalize_blank_lines(contents: str) -> str: + """Collapse runs of blank lines to one blank line.""" + + return re.sub(r"\n{3,}", "\n\n", contents) diff --git a/scripts/docs_skill/_helpers/navigation.py b/scripts/docs_skill/_helpers/navigation.py new file mode 100644 index 0000000..e72c6cb --- /dev/null +++ b/scripts/docs_skill/_helpers/navigation.py @@ -0,0 +1,65 @@ +"""Documentation navigation discovery for skill generation.""" + +import json +from pathlib import Path +from typing import Any + +from scripts.docs_skill.constants import DOCS_SLUG, INDEX_PAGE, UNGROUPED_LABEL +from scripts.docs_skill.models import NavigationGroup + + +def list_ordered_page_paths(*, docs_root: Path, sidebar_path: Path | None = None) -> list[Path]: + """Return existing pages in sidebar order followed by unlisted pages.""" + + paths: list[Path] = [] + for group in list_navigation_groups(docs_root=docs_root, sidebar_path=sidebar_path): + paths.extend(group.page_paths) + return paths + + +def _collect_navigation_pages(node: Any) -> list[str]: + if isinstance(node, str): + return [INDEX_PAGE if node == DOCS_SLUG else node.removeprefix(f"{DOCS_SLUG}/")] + if isinstance(node, list): + pages: list[str] = [] + for item in node: + pages.extend(_collect_navigation_pages(item)) + return pages + if isinstance(node, dict): + return _collect_navigation_pages(node.get("slug", node.get("items", []))) + return [] + + +def list_navigation_groups( + *, docs_root: Path, sidebar_path: Path | None = None +) -> list[NavigationGroup]: + """Flatten nested sidebar groups within each top-level group, then add unlisted pages.""" + + sidebar_path = sidebar_path or docs_root.resolve().parents[2] / "sidebar.json" + nodes: list[Any] = ( + json.loads(sidebar_path.read_text(encoding="utf-8")) if sidebar_path.exists() else [] + ) + available: dict[str, Path] = {} + for suffix in ("*.md", "*.mdx"): + for path in sorted(docs_root.rglob(suffix)): + relative_path: Path = path.relative_to(docs_root) + available[relative_path.with_suffix("").as_posix()] = relative_path + groups: list[NavigationGroup] = [] + seen: set[str] = set() + for node in nodes: + members: list[Path] = [] + for page in _collect_navigation_pages(node): + if page in available and page not in seen: + members.append(available[page]) + seen.add(page) + if members: + label: str = ( + node.get("label", UNGROUPED_LABEL) if isinstance(node, dict) else UNGROUPED_LABEL + ) + groups.append(NavigationGroup(label=label, page_paths=tuple(members))) + remaining: tuple[Path, ...] = tuple( + available[page] for page in sorted(available) if page not in seen + ) + if remaining: + groups.append(NavigationGroup(label=UNGROUPED_LABEL, page_paths=remaining)) + return groups diff --git a/scripts/docs_skill/_helpers/output.py b/scripts/docs_skill/_helpers/output.py new file mode 100644 index 0000000..4c07d74 --- /dev/null +++ b/scripts/docs_skill/_helpers/output.py @@ -0,0 +1,47 @@ +"""Generated docs reference output handling.""" + +from pathlib import Path + +from scripts.docs_skill.constants import GENERATED_MARKER + + +def write_reference_pages(*, output_dir: Path, pages: dict[str, str]) -> None: + """Write generated reference pages and remove stale generated pages.""" + + output_dir.mkdir(parents=True, exist_ok=True) + for relative_path, contents in pages.items(): + destination: Path = output_dir / relative_path + destination.parent.mkdir(parents=True, exist_ok=True) + destination.write_text(contents, encoding="utf-8") + stale_paths: list[Path] = [ + path + for path in output_dir.rglob("*.md") + if path.relative_to(output_dir).as_posix() not in pages + and GENERATED_MARKER in path.read_text(encoding="utf-8") + ] + for stale_path in stale_paths: + stale_path.unlink() + for directory in sorted(output_dir.rglob("*"), key=lambda path: len(path.parts), reverse=True): + if directory.is_dir() and not any(directory.iterdir()): + directory.rmdir() + print(f"Wrote {len(pages)} reference pages to {output_dir} ({len(stale_paths)} removed)") + + +def outdated_reference_pages(*, output_dir: Path, pages: dict[str, str]) -> tuple[str, ...]: + """Return generated pages that are missing, differ from `pages`, or are no longer generated.""" + + existing: dict[str, str] = { + path.relative_to(output_dir).as_posix(): path.read_text(encoding="utf-8") + for path in output_dir.rglob("*.md") + } + changed: set[str] = { + relative_path + for relative_path, contents in pages.items() + if existing.get(relative_path) != contents + } + stale: set[str] = { + relative_path + for relative_path, contents in existing.items() + if relative_path not in pages and GENERATED_MARKER in contents + } + return tuple(sorted(changed | stale)) diff --git a/scripts/docs_skill/_helpers/render.py b/scripts/docs_skill/_helpers/render.py new file mode 100644 index 0000000..257b3b2 --- /dev/null +++ b/scripts/docs_skill/_helpers/render.py @@ -0,0 +1,110 @@ +"""Markdown rendering for bundled SQLClean docs reference pages.""" + +import os +import re +from pathlib import Path, PurePosixPath + +from scripts.docs_skill._helpers.mdx import normalize_blank_lines, parse_mdx_page +from scripts.docs_skill._helpers.navigation import list_navigation_groups +from scripts.docs_skill.constants import ( + DOCS_BASE_URL, + DOCS_URL_PATH, + GENERATED_MARKER, + INDEX_FILENAME, + INDEX_PAGE, + SITE_BASE_URL, + TABLE_OF_CONTENTS_MIN_LINES, +) +from scripts.docs_skill.models import MdxPage, NavigationGroup + +_INTERNAL_LINK: re.Pattern[str] = re.compile(r"\]\((/[^)\s#]*)(#[^)\s]*)?\)") +_SECTION_HEADING: re.Pattern[str] = re.compile(r"^## (.+)$", re.MULTILINE) + + +def build_reference_pages(*, docs_root: Path, sidebar_path: Path | None = None) -> dict[str, str]: + """Render every documentation page plus a grouped index, keyed by relative output path.""" + + groups: list[NavigationGroup] = list_navigation_groups( + docs_root=docs_root, sidebar_path=sidebar_path + ) + page_names: set[str] = set() + for group in groups: + page_names.update(path.with_suffix("").as_posix() for path in group.page_paths) + known_pages: frozenset[str] = frozenset(page_names) + pages: dict[str, MdxPage] = {} + rendered: dict[str, str] = {} + for group in groups: + for page_path in group.page_paths: + page_name: str = page_path.with_suffix("").as_posix() + page: MdxPage = parse_mdx_page((docs_root / page_path).read_text(encoding="utf-8")) + pages[page_name] = page + rendered[f"{page_name}.md"] = _render_page( + page=page, page_name=page_name, known_pages=known_pages + ) + rendered[INDEX_FILENAME] = _render_index(groups=groups, pages=pages) + return rendered + + +def _render_page(*, page: MdxPage, page_name: str, known_pages: frozenset[str]) -> str: + body: str = _rewrite_internal_links( + body=page.body, page_name=page_name, known_pages=known_pages + ) + description_block: str = f"> {page.description}\n\n" if page.description else "" + contents_block: str = _table_of_contents(body=body) + return ( + normalize_blank_lines( + f"{GENERATED_MARKER}\n\n# {page.title}\n\n{description_block}" + f"Online: {DOCS_BASE_URL}/{'' if page_name == INDEX_PAGE else page_name + '/'}" + f"\n\n{contents_block}{body}" + ).strip() + + "\n" + ) + + +def _table_of_contents(*, body: str) -> str: + if body.count("\n") < TABLE_OF_CONTENTS_MIN_LINES: + return "" + headings: list[str] = _SECTION_HEADING.findall(_without_code_blocks(body)) + if not headings: + return "" + return "## Contents\n\n" + "".join(f"- {heading}\n" for heading in headings) + "\n" + + +def _without_code_blocks(body: str) -> str: + return re.sub(r"```.*?```", "", body, flags=re.DOTALL) + + +def _rewrite_internal_links(*, body: str, page_name: str, known_pages: frozenset[str]) -> str: + source_dir: PurePosixPath = PurePosixPath(page_name).parent + + def replace(match: re.Match[str]) -> str: + path: str = match.group(1) + anchor: str = match.group(2) or "" + if path != DOCS_URL_PATH and not path.startswith(f"{DOCS_URL_PATH}/"): + return f"]({SITE_BASE_URL}{path}{anchor})" + target: str = path.removeprefix(DOCS_URL_PATH).strip("/") or INDEX_PAGE + if target not in known_pages: + return f"]({DOCS_BASE_URL}/{target}/{anchor})" + relative: str = os.path.relpath(f"{target}.md", start=source_dir.as_posix() or ".") + return f"]({PurePosixPath(relative).as_posix()}{anchor})" + + return _INTERNAL_LINK.sub(replace, body) + + +def _render_index(*, groups: list[NavigationGroup], pages: dict[str, MdxPage]) -> str: + sections: list[str] = [] + for group in groups: + lines: list[str] = [f"## {group.label}", ""] + for page_path in group.page_paths: + page_name: str = page_path.with_suffix("").as_posix() + page: MdxPage = pages[page_name] + summary: str = f" - {page.description}" if page.description else "" + lines.append(f"- [{page.title}]({page_name}.md) (`{page_name}`){summary}") + sections.append("\n".join(lines)) + return ( + f"{GENERATED_MARKER}\n\n# SQLClean documentation index\n\n" + "Bundled copies of every page on the SQLClean documentation site, matching the " + "installed SQLClean version. Open only the page you need.\n\n" + + "\n\n".join(sections) + + "\n" + ) diff --git a/scripts/docs_skill/constants.py b/scripts/docs_skill/constants.py new file mode 100644 index 0000000..80c049e --- /dev/null +++ b/scripts/docs_skill/constants.py @@ -0,0 +1,18 @@ +"""Stable constants for SQLClean docs reference generation.""" + +from pathlib import Path + +REPOSITORY_ROOT: Path = Path(__file__).resolve().parents[2] +DEFAULT_DOCS_ROOT: Path = REPOSITORY_ROOT / "website/src/content/docs/docs" +DEFAULT_OUTPUT_DIR: Path = REPOSITORY_ROOT / "crates/sqlclean-cli/skill/references/docs" +SITE_BASE_URL: str = "https://sqlclean.io" +DOCS_BASE_URL: str = f"{SITE_BASE_URL}/docs" +DOCS_SLUG: str = "docs" +DOCS_URL_PATH: str = f"/{DOCS_SLUG}" +INDEX_PAGE: str = "index" +COMPONENT_TAG_END: str = ">" +GENERATED_MARKER: str = "" +INDEX_FILENAME: str = "CONTENTS.md" +FRONTMATTER_DELIMITER: str = "---" +TABLE_OF_CONTENTS_MIN_LINES: int = 100 +UNGROUPED_LABEL: str = "Other pages" diff --git a/scripts/docs_skill/main/__init__.py b/scripts/docs_skill/main/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/scripts/docs_skill/main/check.py b/scripts/docs_skill/main/check.py new file mode 100644 index 0000000..4202fd7 --- /dev/null +++ b/scripts/docs_skill/main/check.py @@ -0,0 +1,41 @@ +"""Focused entry that fails when the bundled docs reference pages are out of date.""" + +from __future__ import annotations + +import argparse +import sys +from pathlib import Path + +from scripts.docs_skill._helpers.output import outdated_reference_pages +from scripts.docs_skill._helpers.render import build_reference_pages +from scripts.docs_skill.constants import DEFAULT_DOCS_ROOT, DEFAULT_OUTPUT_DIR + + +def check_docs_skill(argv: list[str] | None = None) -> int: + """Compare freshly generated reference pages with the committed ones.""" + + args: argparse.Namespace = _parse_args(argv) + outdated: tuple[str, ...] = outdated_reference_pages( + output_dir=args.output_dir, + pages=build_reference_pages(docs_root=args.docs_root), + ) + if not outdated: + print(f"Docs skill reference pages in {args.output_dir} are up to date") + return 0 + print( + f"{len(outdated)} docs skill reference page(s) in {args.output_dir} are out of date " + "with the website docs. Run `make skills` and commit the result:", + file=sys.stderr, + ) + for relative_path in outdated: + print(f" {relative_path}", file=sys.stderr) + return 1 + + +def _parse_args(argv: list[str] | None) -> argparse.Namespace: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Fail when bundled SQLClean skill reference pages differ from the website docs." + ) + parser.add_argument("--docs-root", type=Path, default=DEFAULT_DOCS_ROOT) + parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR) + return parser.parse_args(argv) diff --git a/scripts/docs_skill/main/generate.py b/scripts/docs_skill/main/generate.py new file mode 100644 index 0000000..eee8b95 --- /dev/null +++ b/scripts/docs_skill/main/generate.py @@ -0,0 +1,30 @@ +"""Focused entry for SQLClean docs reference generation.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from scripts.docs_skill._helpers.output import write_reference_pages +from scripts.docs_skill._helpers.render import build_reference_pages +from scripts.docs_skill.constants import DEFAULT_DOCS_ROOT, DEFAULT_OUTPUT_DIR + + +def generate_docs_skill(argv: list[str] | None = None) -> int: + """Generate the bundled documentation reference pages for the SQLClean skill.""" + + args: argparse.Namespace = _parse_args(argv) + write_reference_pages( + output_dir=args.output_dir, + pages=build_reference_pages(docs_root=args.docs_root), + ) + return 0 + + +def _parse_args(argv: list[str] | None) -> argparse.Namespace: + parser: argparse.ArgumentParser = argparse.ArgumentParser( + description="Convert the SQLClean website docs into bundled skill reference pages." + ) + parser.add_argument("--docs-root", type=Path, default=DEFAULT_DOCS_ROOT) + parser.add_argument("--output-dir", type=Path, default=DEFAULT_OUTPUT_DIR) + return parser.parse_args(argv) diff --git a/scripts/docs_skill/models.py b/scripts/docs_skill/models.py new file mode 100644 index 0000000..79fa7fe --- /dev/null +++ b/scripts/docs_skill/models.py @@ -0,0 +1,21 @@ +"""Structured models for SQLClean docs reference generation.""" + +from dataclasses import dataclass +from pathlib import Path + + +@dataclass(frozen=True, slots=True) +class MdxPage: + """Content extracted from one MDX documentation page.""" + + title: str + description: str + body: str + + +@dataclass(frozen=True, slots=True) +class NavigationGroup: + """One navigation group label and its pages in documentation order.""" + + label: str + page_paths: tuple[Path, ...] diff --git a/scripts/generate_docs_skill.py b/scripts/generate_docs_skill.py new file mode 100644 index 0000000..360aede --- /dev/null +++ b/scripts/generate_docs_skill.py @@ -0,0 +1,15 @@ +"""Direct wrapper for SQLClean docs skill generation.""" + +from __future__ import annotations + +from scripts.docs_skill.main.generate import generate_docs_skill + + +def main(argv: list[str] | None = None) -> int: + """Generate the SQLClean documentation skill.""" + + return generate_docs_skill(argv) + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tests/e2e/src/sqlclean/hosting/_test_types.py b/tests/e2e/src/sqlclean/hosting/_test_types.py index 9e0aab7..e47c068 100644 --- a/tests/e2e/src/sqlclean/hosting/_test_types.py +++ b/tests/e2e/src/sqlclean/hosting/_test_types.py @@ -80,3 +80,14 @@ class ManyFilesTestCase: file_count: int flagged_every: int expected_finding_count: int + + +@dataclass(frozen=True) +class SkillsTestCase: + """Files already in a project, a skills invocation and the skill entries it must leave.""" + + description: str + existing: dict[str, str] + arguments: tuple[str, ...] + expected_exit: int + expected_entries: dict[str, bool] diff --git a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py index 289c497..279a38a 100644 --- a/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py +++ b/tests/e2e/src/sqlclean/hosting/test_sqlclean_command.py @@ -15,6 +15,7 @@ ManyFilesTestCase, RewriteTestCase, SarifTestCase, + SkillsTestCase, SummaryTestCase, ) from tests.e2e.src.sqlclean.hosting.helpers import run, write_project @@ -383,3 +384,68 @@ def test_given_many_files_when_checking_custom_rules_then_every_host_share_repor assert [finding["path"] for finding in report["findings"]] == [ f"models/m{index:04}.sql" for index in flagged ] + + +GENERATED: str = "" +STALE_PAGE: str = ".claude/skills/sqlclean/references/docs/removed.md" + + +@pytest.mark.parametrize( + "test_case", + [ + SkillsTestCase( + description="default targets get the skill and its bundled docs", + existing={}, + arguments=(), + expected_exit=0, + expected_entries={ + ".agents/skills/sqlclean/SKILL.md": True, + ".agents/skills/sqlclean/references/docs/CONTENTS.md": True, + ".claude/skills/sqlclean/references/docs/configuration.md": True, + ".opencode/skills/sqlclean/SKILL.md": False, + }, + ), + SkillsTestCase( + description="a generated page the package no longer ships is removed", + existing={STALE_PAGE: GENERATED + "\n"}, + arguments=("--target", "claude"), + expected_exit=0, + expected_entries={STALE_PAGE: False, ".claude/skills/sqlclean/SKILL.md": True}, + ), + SkillsTestCase( + description="a hand-written skill file is not overwritten", + existing={".claude/skills/sqlclean/SKILL.md": "my notes\n"}, + arguments=(), + expected_exit=2, + expected_entries={ + ".claude/skills/sqlclean/SKILL.md": False, + ".agents/skills/sqlclean/SKILL.md": False, + }, + ), + SkillsTestCase( + description="--force replaces a hand-written skill file", + existing={".claude/skills/sqlclean/SKILL.md": "my notes\n"}, + arguments=("--force", "--target", "claude"), + expected_exit=0, + expected_entries={".claude/skills/sqlclean/SKILL.md": True}, + ), + ], + ids=lambda case: case.description, +) +def test_given_project_when_installing_skills_then_only_generated_files_are_written( + tmp_path: Path, test_case: SkillsTestCase +) -> None: + write_project(tmp_path, config=DUCKDB, files=test_case.existing) + + completed: subprocess.CompletedProcess[str] = run( + tmp_path, "skills", *test_case.arguments, environment={} + ) + + assert completed.returncode == test_case.expected_exit, completed.stderr + generated: dict[str, bool] = { + path.relative_to(tmp_path).as_posix(): GENERATED in path.read_text(encoding="utf-8") + for path in tmp_path.rglob("*.md") + } + assert { + path: generated.get(path, False) for path in test_case.expected_entries + } == test_case.expected_entries diff --git a/website/src/content/docs/docs/cli/skills.mdx b/website/src/content/docs/docs/cli/skills.mdx new file mode 100644 index 0000000..f55a5c4 --- /dev/null +++ b/website/src/content/docs/docs/cli/skills.mdx @@ -0,0 +1,60 @@ +--- +title: "sqlclean skills" +description: "Install the SQLClean skill so AI coding agents know its commands, Rules and configuration." +sidebar: + label: "skills" +--- + +`sqlclean skills` writes the SQLClean skill into your project, so AI coding agents such as Claude +Code, OpenCode and Cursor know how to check, fix and format SQL, configure `sqlclean.toml` and +write custom Rules. The skill ships inside the `sqlclean` package with a copy of every page of +these docs, so it always matches the installed version. + +## Usage + +```bash +sqlclean skills [--target ]... [--global] [--force] +``` + +```bash +# Install the agents and claude targets (default) +sqlclean skills + +# Install for Claude Code only +sqlclean skills --target claude + +# Install under your home directory for every project +sqlclean skills --global +``` + +## Flags + +| Flag | Description | +|------|-------------| +| `--target ` | The agent to install for: `agents`, `claude` or `opencode`. Repeat it for several. | +| `--global` | Install under the home directory instead of the configuration root | +| `--force` | Overwrite skill files that SQLClean did not generate | + +## Targets + +| Target | Project path | Global path | +|--------|--------------|-------------| +| `agents` | `.agents/skills/sqlclean/` | `~/.agents/skills/sqlclean/` | +| `claude` | `.claude/skills/sqlclean/` | `~/.claude/skills/sqlclean/` | +| `opencode` | `.opencode/skills/sqlclean/` | `~/.config/opencode/skills/sqlclean/` | + +Without `--target`, SQLClean installs `agents` and `claude`. OpenCode and several other agents +read the shared `.agents` directory; Claude Code needs its own `.claude` copy. + +The project path is relative to the directory of the nearest `sqlclean.toml`, or the working +directory when there is none. + +## Updating + +Every file SQLClean writes carries a `generated-by: sqlclean skills` comment. Rerun +`sqlclean skills` after upgrading SQLClean: + +- Generated files are rewritten with the new version's content. +- Generated pages the new version no longer ships are removed. +- A file without the comment, such as one you wrote yourself, is never overwritten. The command + stops with exit status 2 before writing anything, unless you pass `--force`. diff --git a/website/src/sidebar.json b/website/src/sidebar.json index 301b00c..fde9167 100644 --- a/website/src/sidebar.json +++ b/website/src/sidebar.json @@ -48,7 +48,8 @@ "docs/cli/fix", "docs/cli/format", "docs/cli/rules", - "docs/cli/facts" + "docs/cli/facts", + "docs/cli/skills" ] } ] From 116ffb0cc834664e1c71a05d9d5f0fb2600ed6ab Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 21:29:10 +0000 Subject: [PATCH 28/29] ci: run only the suites a change needs and keep site edits out of releases Verify classifies a pull request by changed paths. Website, docs and benchmark edits run the docs skill check and ruff; bundled skill edits add a Linux build and the skills tests; anything else runs everything. The Verify job aggregates the selected suites. Release Please counts every Conventional Commit regardless of hidden changelog sections, so exclude-paths keeps website, benchmark and workflow-only commits from cutting a release. Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- .github/RELEASING.md | 40 ++++++++++++ .github/scripts/classify_changes.py | 55 ++++++++++++++++ .github/workflows/release-please.yml | 11 +--- .github/workflows/verify.yml | 97 ++++++++++++++++++++++++++-- release-please-config.json | 57 +++++++++++++--- website/README.md | 6 ++ 6 files changed, 240 insertions(+), 26 deletions(-) create mode 100644 .github/RELEASING.md create mode 100644 .github/scripts/classify_changes.py diff --git a/.github/RELEASING.md b/.github/RELEASING.md new file mode 100644 index 0000000..57ab811 --- /dev/null +++ b/.github/RELEASING.md @@ -0,0 +1,40 @@ +# Releasing + +The website and the packages ship separately. + +## Website + +Cloudflare Pages builds `website/` (`npm ci && npm run build`, output `dist`) from its own Git +integration. A push to `main` deploys https://sqlclean.io; every other branch deploys a preview +at `.sqlclean.pages.dev`. No workflow here deploys the site, and publishing packages never +touches it. + +## Packages + +Release Please keeps a release pull request open on `main`. Merging it tags `v0.MINOR.PATCH` and +dispatches `publish.yml`, which builds the `sqlclean-cli` wheels and both sdists and publishes +`sqlclean` and `sqlclean-cli` to PyPI. + +Release Please counts every Conventional Commit on `main` since the last release, whatever its +type: `docs:` and `chore:` commits bump the patch version even though their changelog sections +are hidden or not. What keeps a change out of a release is `exclude-paths` in +`release-please-config.json`. A commit whose files all sit under `website/`, `benchmarks/` or +`.github/` is ignored, so site, benchmark and workflow edits never cut a release. + +Documentation edits usually do release, on purpose: `make skills` copies the docs into +`crates/sqlclean-cli/skill/`, which the `sqlclean` executable embeds for `sqlclean skills`, so the +installed skill stays in step with the docs. A homepage or styling change touches only `website/` +and does not. + +## Verify + +`verify.yml` classifies a pull request by the paths it changes and runs only the suites it needs. +The `Verify` job is the one check to require: it passes when every selected suite passed. + +| Change | Suites | +|--------|--------| +| Only `website/`, `benchmarks/`, root `*.md`, `.github/demos/`, images, `website.yml` | docs skill check and ruff | +| Also the bundled skill under `crates/sqlclean-cli/skill/` | plus a Linux build and the `sqlclean skills` tests | +| Anything else, including workflows, Rust, Python, tests and lockfiles | everything: `make verify`, minimum Rust, Windows, three-platform package smoke tests and the source build | + +A manual or release dispatch has no pull request diff, so it runs everything. diff --git a/.github/scripts/classify_changes.py b/.github/scripts/classify_changes.py new file mode 100644 index 0000000..82929ef --- /dev/null +++ b/.github/scripts/classify_changes.py @@ -0,0 +1,55 @@ +"""Decide which Verify suites a change needs from the paths it touches, one per line on stdin. + +Prints `full=` and `skills=` lines for GITHUB_OUTPUT: + +- Documentation, website, benchmark and image changes need only the light checks. +- Changes confined to the bundled agent skill also rebuild the executable on Linux and run the + skills command tests, because the executable embeds the skill. +- Any other change, including workflows, Rust, Python, tests and lockfiles, runs every suite. + +An empty path list means the diff could not be computed, so every suite runs. +""" + +from __future__ import annotations + +import fnmatch +import sys + +LIGHT: tuple[str, ...] = ( + "website/*", + "benchmarks/*", + ".github/demos/*", + ".github/*.png", + ".github/*.md", + ".github/workflows/website.yml", + "*.md", +) +SKILL: tuple[str, ...] = ("crates/sqlclean-cli/skill/*",) + + +def classify(paths: list[str]) -> dict[str, bool]: + """Return which suites the changed paths need.""" + + def matches(path: str, patterns: tuple[str, ...]) -> bool: + # `*.md` must only match a root file, while `website/*` matches every depth. + return any( + fnmatch.fnmatch(path, pattern) and ("/" in pattern or "/" not in path) + for pattern in patterns + ) + + skills: bool = any(matches(path, SKILL) for path in paths) + full: bool = not paths or any( + not matches(path, LIGHT) and not matches(path, SKILL) for path in paths + ) + return {"full": full, "skills": skills and not full} + + +def main() -> int: + paths: list[str] = [line.strip() for line in sys.stdin if line.strip()] + for name, value in classify(paths).items(): + print(f"{name}={str(value).lower()}") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/.github/workflows/release-please.yml b/.github/workflows/release-please.yml index 8d3bf9e..6d6d6dc 100644 --- a/.github/workflows/release-please.yml +++ b/.github/workflows/release-please.yml @@ -180,16 +180,7 @@ jobs: workflows=(pull-request-metadata.yml version-guard.yml verify.yml) metadata_contexts=("Pull Request Metadata") version_contexts=("Block automatic 1.0 releases") - verify_contexts=( - "verify" - "rust" - "Minimum supported Rust (1.88)" - "windows" - "native-package-smoke (ubuntu-latest)" - "native-package-smoke (macos-latest)" - "native-package-smoke (windows-latest)" - "source-build" - ) + verify_contexts=("Verify") required_contexts=( "${metadata_contexts[@]}" "${version_contexts[@]}" diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8068a2f..6b79a57 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -12,23 +12,62 @@ concurrency: cancel-in-progress: true jobs: - verify: + changes: + name: Classify changes + runs-on: ubuntu-latest + outputs: + full: ${{ steps.classify.outputs.full }} + skills: ${{ steps.classify.outputs.skills }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - id: classify + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # Without an exact pull request diff, such as a manual or release dispatch, run everything. + if [[ -n "${BASE_SHA}" && -n "${HEAD_SHA}" ]] \ + && changed="$(git diff --name-only "${BASE_SHA}...${HEAD_SHA}")"; then + printf '%s\n' "${changed}" + else + changed="" + fi + printf '%s\n' "${changed}" | python3 -I .github/scripts/classify_changes.py | tee -a "${GITHUB_OUTPUT}" + + light: + name: Docs skill and lint + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: astral-sh/setup-uv@v6 + - run: python3 -m scripts.check_docs_skill + - run: uvx ruff@0.17.3 format --check . + - run: uvx ruff@0.17.3 check . + + skills: + name: Skills command + needs: [changes] + if: needs.changes.outputs.skills == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@master with: toolchain: 1.95.0 - components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 with: enable-cache: true - run: uv python install 3.12 - run: uv sync --locked --all-groups - - run: make verify + - run: uv run pytest tests/e2e -q -n auto -k skills - rust: + checks: + name: Checks and tests + needs: [changes] + if: needs.changes.outputs.full == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -38,13 +77,16 @@ jobs: components: rustfmt, clippy - uses: Swatinem/rust-cache@v2 - uses: astral-sh/setup-uv@v6 + with: + enable-cache: true - run: uv python install 3.12 - run: uv sync --locked --all-groups - - run: make check-rust - - run: make test-rust + - run: make verify minimum-rust: name: Minimum supported Rust (1.88) + needs: [changes] + if: needs.changes.outputs.full == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -55,6 +97,8 @@ jobs: - run: cargo +1.88.0 check --workspace --all-targets --locked windows: + needs: [changes] + if: needs.changes.outputs.full == 'true' runs-on: windows-latest steps: - uses: actions/checkout@v4 @@ -71,6 +115,8 @@ jobs: - run: uv run pytest tests -q -n auto native-package-smoke: + needs: [changes] + if: needs.changes.outputs.full == 'true' strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -88,6 +134,8 @@ jobs: - run: uv run pytest tests/e2e -q source-build: + needs: [changes] + if: needs.changes.outputs.full == 'true' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -103,3 +151,40 @@ jobs: - run: uv venv .source-venv --python 3.12 - run: uv pip install --python .source-venv/bin/python --no-deps dist/*.tar.gz - run: .source-venv/bin/sqlclean --version + - name: The source build embeds the skill + run: | + cd "$(mktemp -d)" + "${GITHUB_WORKSPACE}/.source-venv/bin/sqlclean" skills --target agents + test -s .agents/skills/sqlclean/references/docs/CONTENTS.md + + verify: + # The single check to require: it passes when every suite the change selected passed. + name: Verify + if: always() + needs: [changes, light, skills, checks, minimum-rust, windows, native-package-smoke, source-build] + runs-on: ubuntu-latest + steps: + - env: + CHANGES: ${{ needs.changes.result }} + LIGHT: ${{ needs.light.result }} + FULL: ${{ needs.changes.outputs.full }} + CHECKS: ${{ needs.checks.result }} + SUITES: >- + ${{ needs.skills.result }} ${{ needs.minimum-rust.result }} ${{ needs.windows.result }} + ${{ needs.native-package-smoke.result }} ${{ needs.source-build.result }} + run: | + if [[ "${CHANGES}" != success || "${LIGHT}" != success ]]; then + echo "Change classification or the light checks did not pass" >&2 + exit 1 + fi + if [[ "${FULL}" == true && "${CHECKS}" != success ]]; then + echo "The full suite was selected but did not pass: ${CHECKS}" >&2 + exit 1 + fi + for result in ${CHECKS} ${SUITES}; do + if [[ "${result}" != success && "${result}" != skipped ]]; then + echo "A selected suite did not pass: ${result}" >&2 + exit 1 + fi + done + echo "Every selected suite passed (full=${FULL})" diff --git a/release-please-config.json b/release-please-config.json index c483077..e6eeb12 100644 --- a/release-please-config.json +++ b/release-please-config.json @@ -8,16 +8,48 @@ "bump-minor-pre-major": true, "bump-patch-for-minor-pre-major": false, "changelog-sections": [ - { "type": "feat", "section": "Features" }, - { "type": "fix", "section": "Bug Fixes" }, - { "type": "revert", "section": "Bug Fixes" }, - { "type": "perf", "section": "Performance Improvements" }, - { "type": "refactor", "section": "Refactoring" }, - { "type": "build", "section": "Build System" }, - { "type": "docs", "section": "Documentation" }, - { "type": "chore", "section": "Maintenance" }, - { "type": "ci", "section": "Continuous Integration", "hidden": true }, - { "type": "test", "section": "Tests", "hidden": true } + { + "type": "feat", + "section": "Features" + }, + { + "type": "fix", + "section": "Bug Fixes" + }, + { + "type": "revert", + "section": "Bug Fixes" + }, + { + "type": "perf", + "section": "Performance Improvements" + }, + { + "type": "refactor", + "section": "Refactoring" + }, + { + "type": "build", + "section": "Build System" + }, + { + "type": "docs", + "section": "Documentation" + }, + { + "type": "chore", + "section": "Maintenance" + }, + { + "type": "ci", + "section": "Continuous Integration", + "hidden": true + }, + { + "type": "test", + "section": "Tests", + "hidden": true + } ], "extra-files": [ { @@ -32,6 +64,11 @@ "type": "generic", "path": "packages/sqlclean-cli/pyproject.toml" } + ], + "exclude-paths": [ + "website", + "benchmarks", + ".github" ] } } diff --git a/website/README.md b/website/README.md index 9c39df1..c0a2f6a 100644 --- a/website/README.md +++ b/website/README.md @@ -15,6 +15,12 @@ The SQLClean website: the homepage at `/` and the documentation at `/docs/`, bui Search uses Pagefind, and `llms.txt`, `llms-small.txt` and `llms-full.txt` are generated at build time. The Website workflow builds the site and checks its links on every change; it does not deploy. +Cloudflare Pages deploys it: every push to `main` publishes https://sqlclean.io, and every other +branch gets a preview at `.sqlclean.pages.dev`. See [Releasing](../.github/RELEASING.md). + +The pages under `src/content/docs/docs/` are also bundled into the `sqlclean skills` agent skill. +After editing them, run `make skills` from the repository root and commit the regenerated +`crates/sqlclean-cli/skill/references/docs/`; CI fails while the two differ. Requires Node 22.12 or newer. From 261abfb1fa5254485a9a718b78a71138879057aa Mon Sep 17 00:00:00 2001 From: Amp Date: Fri, 30 Oct 2026 21:44:08 +0000 Subject: [PATCH 29/29] test: write e2e project files with LF line endings on every platform Amp-Thread-ID: https://ampcode.com/threads/T-01a18b8a-340f-717d-9ca7-6dcf564a065a Co-authored-by: Kevin Longe --- tests/e2e/src/sqlclean/hosting/helpers.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/e2e/src/sqlclean/hosting/helpers.py b/tests/e2e/src/sqlclean/hosting/helpers.py index 4d1a636..a4c972f 100644 --- a/tests/e2e/src/sqlclean/hosting/helpers.py +++ b/tests/e2e/src/sqlclean/hosting/helpers.py @@ -20,13 +20,13 @@ def executable() -> str: def write_project(root: Path, *, config: str, files: dict[str, str]) -> None: - """Write sqlclean.toml and every file under root.""" + """Write sqlclean.toml and every file under root with LF line endings on every platform.""" - (root / "sqlclean.toml").write_text(config, encoding="utf-8") + (root / "sqlclean.toml").write_text(config, encoding="utf-8", newline="\n") for path, text in files.items(): target: Path = root / path target.parent.mkdir(parents=True, exist_ok=True) - target.write_text(text, encoding="utf-8") + target.write_text(text, encoding="utf-8", newline="\n") def run(

    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 diff --git a/.github/workflows/website.yml b/.github/workflows/website.yml new file mode 100644 index 0000000..a22350f --- /dev/null +++ b/.github/workflows/website.yml @@ -0,0 +1,45 @@ +name: Website + +on: + push: + branches: [main] + paths: + - "website/**" + - ".github/workflows/website.yml" + pull_request: + paths: + - "website/**" + - ".github/workflows/website.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: website-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + name: Build the website + runs-on: ubuntu-24.04 + defaults: + run: + working-directory: website + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + cache-dependency-path: website/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build + run: npm run build + + - name: Check links + run: python3 scripts/check_links.py dist diff --git a/AGENTS.md b/AGENTS.md index 3798a2a..26d2548 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,6 +9,8 @@ - `src/sqlclean`: the Python package. It owns the `@rule` authoring API, the custom Rule host that the executable starts, and the `sqlclean.testing` harness. - `packages/sqlclean-cli`: the maturin wheel that ships the executable. +- `website`: the homepage and the docs (Astro Starlight). Docs pages follow `website/STYLE.md`; + change the page a behaviour change affects in the same commit. ## Conventions diff --git a/README.md b/README.md index b12af69..d8ead1e 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ dialect = "snowflake" [format] line_width = 100 +indent = 4 [rules] select = ["SQC"] # omit to run the Rules enabled by default diff --git a/website/.gitignore b/website/.gitignore new file mode 100644 index 0000000..ddce69b --- /dev/null +++ b/website/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +.astro/ diff --git a/website/README.md b/website/README.md new file mode 100644 index 0000000..9c39df1 --- /dev/null +++ b/website/README.md @@ -0,0 +1,27 @@ +# Website + +The SQLClean website: the homepage at `/` and the documentation at `/docs/`, built as one +[Astro Starlight](https://starlight.astro.build) site. + +- `public/index.html` and `public/styles.css`: the homepage, plain static HTML and CSS. Its terminal + examples are real `sqlclean` output from small example projects, trimmed to the relevant lines. +- `src/content/docs/docs/`: the documentation pages (MDX). `src/sidebar.json` holds the sidebar + and `src/navigation.mjs` the redirects. `STYLE.md` is the style guide for these pages. +- `src/components/`: the page layout, header and page title, replacing Starlight's defaults. + `src/components/docs/` has the `Card`, `CardGroup`, `Frame`, `Note` and `Warning` components the + pages use. +- `public/fonts/`: self-hosted fonts, each with its licence. +- `recordings/`: the VHS tapes behind the README demo; see `recordings/README.md`. + +Search uses Pagefind, and `llms.txt`, `llms-small.txt` and `llms-full.txt` are generated at build +time. The Website workflow builds the site and checks its links on every change; it does not deploy. + +Requires Node 22.12 or newer. + +```bash +npm install +npm run dev # live preview +npm run build # static site in dist/, including the search index +npm run preview # serve dist/ +python3 scripts/check_links.py dist +``` diff --git a/website/STYLE.md b/website/STYLE.md new file mode 100644 index 0000000..480f30e --- /dev/null +++ b/website/STYLE.md @@ -0,0 +1,54 @@ +# Docs style guide + +These rules apply to every page under `website/src/content/docs/`. Pages that follow them well: +`rules/overview.mdx`, `cli/check.mdx`, `formatting.mdx` and `custom-rules/overview.mdx`. + +## Write for users + +- Document what a user does, sees and must decide: commands, flags, config keys, defaults, limits, + Rule codes and messages, output, exit codes, and the fix for each problem. +- Leave out how SQLClean works inside: cache files and their formats, hashing, parallelism, the + custom Rule host protocol and internal structures. Include a mechanism only when the user must act + on it, and then say what to do (for example, which flag forces a fresh check). +- Never document behaviour you haven't checked in the source or with the real `sqlclean` command. + Paste real output, trimmed. + +## Structure + +- Lead with what the feature does and a short example: a command, a config block or the output a + user sees. +- Organise sections around the user's tasks ("Recording an exception", "Formatting in CI"), not + around the implementation. +- One topic per section. Put flags, config keys and codes in reference tables near the top or + bottom, and details that rarely matter in a short "Details" or "Limitations" list at the end. +- Keep headings that other pages link to. Before renaming or removing one, run + `grep -rn "#" website/src` and update the links. + +## Tables + +- Use a table for anything that varies by dialect or Rule: support, defaults, fixes and caveats. + Don't write a paragraph per dialect or per Rule. +- Use tables for flags, config keys with defaults, exit codes, Rule codes and fields. +- Keep cells short. Put a long explanation in the text below the table. + +## Sentences + +- Write short, plain sentences in the present tense. One idea per sentence. +- Give every Rule an example where it matters, such as the SQL that triggers a finding and the SQL + that fixes it. +- Use neutral orders-style examples (orders, customers, products, payments). See `AGENTS.md`. + +## No history + +- Describe the product as it is. Don't write "now", "no longer", "used to", "earlier releases", + "as before", "upgrade note" or "previously". History belongs in `CHANGELOG.md`. +- Drop notes about legacy syntax and migrations from old versions once the error message itself + tells the user what to do. + +## Changing a page + +- When a feature changes a page, edit the page into shape: put the new fact in the section it + belongs to, update tables and examples, and remove what is no longer true. Don't append a + paragraph to the end of a section or page. +- Don't repeat a rule that another page owns. Link to it instead, for example to + `rules/overview#exceptions` for how exceptions match findings. diff --git a/website/astro.config.mjs b/website/astro.config.mjs new file mode 100644 index 0000000..2972438 --- /dev/null +++ b/website/astro.config.mjs @@ -0,0 +1,81 @@ +import { defineConfig } from 'astro/config'; +import starlight from '@astrojs/starlight'; +import starlightLlmsTxt from 'starlight-llms-txt'; +import { sqlcleanDark, sqlcleanLight } from './src/code-themes.mjs'; +import { redirects, sidebar } from './src/navigation.mjs'; + +export default defineConfig({ + site: 'https://sqlclean.com', + redirects, + integrations: [ + starlight({ + title: 'SQLClean', + description: 'Fast SQL linting, safe fixes and one canonical format.', + logo: { + light: './src/assets/logo-light.png', + dark: './src/assets/logo-dark.png', + replacesTitle: true, + }, + favicon: '/favicon.png', + head: [ + { tag: 'meta', attrs: { property: 'og:image', content: 'https://sqlclean.com/assets/og-sqlclean-docs.png' } }, + { tag: 'meta', attrs: { property: 'og:image:width', content: '1200' } }, + { tag: 'meta', attrs: { property: 'og:image:height', content: '630' } }, + { tag: 'meta', attrs: { name: 'twitter:image', content: 'https://sqlclean.com/assets/og-sqlclean-docs.png' } }, + ], + social: [ + { icon: 'github', label: 'GitHub', href: 'https://github.com/sqlstack-io/sqlclean' }, + ], + components: { + PageFrame: './src/components/PageFrame.astro', + TwoColumnContent: './src/components/TwoColumnContent.astro', + Header: './src/components/Header.astro', + PageTitle: './src/components/PageTitle.astro', + SiteTitle: './src/components/SiteTitle.astro', + Pagination: './src/components/Pagination.astro', + ThemeSelect: './src/components/ThemeSelect.astro', + }, + customCss: ['./src/styles/fonts.css', './src/styles/sqlclean.css'], + expressiveCode: { + themes: [sqlcleanDark, sqlcleanLight], + defaultProps: { + // Plain boxes for shell snippets, without the fake terminal title bar. + overridesByLang: { 'bash,sh,shell,zsh,console,powershell': { frame: 'none' } }, + }, + styleOverrides: { + borderRadius: '2px', + borderWidth: '1px', + codeFontFamily: 'var(--sl-font-mono)', + codeFontSize: '13px', + codeLineHeight: '1.65', + uiFontFamily: 'var(--sl-font)', + frames: { + shadowColor: 'transparent', + inlineButtonBorder: 'transparent', + inlineButtonBackgroundIdleOpacity: '0', + inlineButtonBackgroundHoverOrFocusOpacity: '0.08', + inlineButtonForeground: 'var(--sqc-muted)', + frameBoxShadowCssValue: 'none', + editorTabBarBackground: 'var(--sqc-code-bg)', + editorActiveTabBackground: 'var(--sqc-code-bg)', + editorActiveTabIndicatorTopColor: 'var(--sqc-primary)', + editorTabBarBorderBottomColor: 'var(--sqc-line)', + terminalTitlebarBackground: 'var(--sqc-code-bg)', + terminalTitlebarBorderBottomColor: 'var(--sqc-line)', + terminalTitlebarDotsOpacity: '0.35', + terminalBackground: 'var(--sqc-code-bg)', + }, + borderColor: 'var(--sqc-line)', + }, + }, + sidebar, + plugins: [ + starlightLlmsTxt({ + projectName: 'SQLClean', + description: + 'SQLClean is a free, open-source SQL linter and formatter with safe fixes and custom Rules in Python.', + }), + ], + }), + ], +}); diff --git a/website/package-lock.json b/website/package-lock.json new file mode 100644 index 0000000..30037c6 --- /dev/null +++ b/website/package-lock.json @@ -0,0 +1,6078 @@ +{ + "name": "sqlclean-website", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "sqlclean-website", + "dependencies": { + "@astrojs/starlight": "^0.42.4", + "@lucide/astro": "^1.48.0", + "astro": "^7.3.5", + "sharp": "^0.35.4", + "starlight-llms-txt": "^0.12.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.0.tgz", + "integrity": "sha512-PxFTx3WoxQnbgUvvwmF6LRQwhHO77a4pewOtJvzxlrYtQ90tLgomLKvks/akrd2iix2ngGlOhxQonKNd8YoBHg==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.0", + "@astrojs/compiler-binding-darwin-arm64": "0.5.0", + "@astrojs/compiler-binding-darwin-x64": "0.5.0", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.0", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.0", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.0", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.0", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.0", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.0", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.0" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.0.tgz", + "integrity": "sha512-wVviC/1KaBqGVF0DbTZ8UtAE3c8TN84CCe7w4ju3SGzo/ByWKn8HCqSU/+1wdltGCCSa30pjVk3H8DQUgnfhRQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.0.tgz", + "integrity": "sha512-5NNIkaGeiztD9QQfhOy+nNrBCZVeHqVZPlfJVjTKq4yJiiSsxAhuBzmVlmK3BDhbSgUT8dai4JV5m6t4/BIQaw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.0.tgz", + "integrity": "sha512-HU/waaccOQDXpvjoWA/9xzadNZIcWKblJoaw19lCxfVs6AHQYdZZOzZUQjY8HaLYGerTXjdADbra2XU+kXk9Og==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.0.tgz", + "integrity": "sha512-a21petBJRi0mNBf+gkRZFoWdGH+VOo0zRQSzM4Nuez5RL5rFFkmbondMejkaRZzvE3LVYU2NVD5MGRgAs4V+nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.0.tgz", + "integrity": "sha512-t5rmu23b/6e6v0bhPibCtwPdABXMRL+bUaJXdkJwTpkINp4zCgH6fNa5Z6+RgkgFSCpmg9uf1MtRIqx1gdmBaA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.0.tgz", + "integrity": "sha512-JWtouuxSiDb2OKKFsxOwfqlpJuZHo1ExirlBGfDgoJVG5zeJWwcd2tU2w4UaJA4suT/tAFSLk/vFhM5fLjWV4A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.0.tgz", + "integrity": "sha512-LlaSO5v3UC6IxuTKXDViWV/fFHYhLKSO9Of118lNBXv/sjIWwziU+jjENxPgxfq/4oUeOu+613y6H93LBPNTOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.0.tgz", + "integrity": "sha512-IP+Cp0m9yKg/AUT47077vHEaAOh/oT/iUZx1PDnV2GJzUxLSEotdQy5Hhv+ycvEadO/DbETSJfAWeV7kte4etQ==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.0.tgz", + "integrity": "sha512-v6plwy5GQBGvfv4QgiGhy/qkkQvYKW7Zh2vBArOnsGEDNHyUS43CWYwIQdw4ZdVVmLsFyhwTiueDZEP6zejddQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.0.tgz", + "integrity": "sha512-SgM158cSCJ248UIXaCCW2WqSTjOoFlDv93ed/ATguJqYpZNNVMlOdcPlgNH7WYj/OYeD0rIQr7XyRAYhB7+G1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.0.tgz", + "integrity": "sha512-2xIypn5R33FRWCXXXv0sfbDj8xcOvc2VmIbteytxMjOzP9LiGfvqaAFJjhdAjzhs6+ZKzYzbSE79rrPObYW5mg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-binding": "0.5.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/internal-helpers": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.11.0.tgz", + "integrity": "sha512-3rzxJ+xbo0+8YyqOzLziIN32wmsHdCjEVz2sGOpRxJ+Ben/KiLph4ItxBy1abEL+E8fkRzqjg0rfXmaHJGw9JA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "js-yaml": "^4.3.0", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.6.0", + "unified": "^11.0.5" + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.2.tgz", + "integrity": "sha512-g4QehXnB/MzxQWnSf17NciFInMqEJy9948p8cc7zGg/r7995m0Ln5PMr3Xlnj24iey1LchaoZ3SzTOa3D4P/3w==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "satteri": "^0.10.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.2.tgz", + "integrity": "sha512-WedIVP2Bu8fXSzxb3ZYZ8xDrWWjf2SRKCneh52Llhoa+O3lcpHPhoZVP3FerlivUaH/0LJz2yVaCNvMdg0u8Bw==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/markdown-satteri": "0.4.2", + "es-module-lexer": "^2.0.0" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "@astrojs/markdown-satteri": "^0.4.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", + "license": "MIT", + "dependencies": { + "prismjs": "^1.30.0" + }, + "engines": { + "node": ">=22.12.0" + } + }, + "node_modules/@astrojs/sitemap": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", + "license": "MIT", + "dependencies": { + "sitemap": "^9.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.42.4", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.4.tgz", + "integrity": "sha512-EsQsqGJ1uTBQcIMojvLqqg4Ku9idwi6PSNuppIOoYmvVP18xjoUkijMdKoDkhmmwKdhbB4ctLhGjPRJzxaq+Jw==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "@astrojs/mdx": "^8.0.0", + "@astrojs/sitemap": "^3.7.3", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.44.0", + "bcp-47": "^2.1.0", + "hast-util-format": "^1.1.0", + "hast-util-select": "^6.0.4", + "hast-util-to-html": "^9.0.5", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^1.2.3", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "remark-directive": "^4.0.0", + "satteri": "^0.10.3", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.6.0" + }, + "engines": { + "node": "18.20.8 || ^20.3.0 || >=22.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", + "cpu": [ + "wasm32" + ], + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", + "license": "MIT", + "dependencies": { + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", + "license": "MIT", + "dependencies": { + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" + } + }, + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", + "license": "MIT", + "dependencies": { + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" + } + }, + "node_modules/@expressive-code/plugin-frames": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" + } + }, + "node_modules/@expressive-code/plugin-text-markers": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2" + } + }, + "node_modules/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "license": "MIT" + }, + "node_modules/@lucide/astro": { + "version": "1.48.0", + "resolved": "https://registry.npmjs.org/@lucide/astro/-/astro-1.48.0.tgz", + "integrity": "sha512-BL+7lLGjjSTfdYEBd9pAB0ixhJ7M84OVzxHoY3oG3M92kX/q1UG2+znp+pqH3waxpIHF1l8Hd1QZxytqv5WH8A==", + "license": "ISC", + "peerDependencies": { + "astro": "^4 || ^5 || ^6 || ^7" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.4.tgz", + "integrity": "sha512-AJxoUD2/15ESHbvpcyjU274nsAPLuOtPHCk0vKJM5pj//Fg/B1FXNWjPnXTT9PymCYYiHo4zPj0ZomXBKhoy7g==", + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.3" + }, + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" + } + }, + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.151.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.151.0.tgz", + "integrity": "sha512-J1yXrIlNDZVzE3ada310xeAw7nH8yCAyLPuUIsjKatFPmfn5bS1oW+cM+QsGOtVWd5nhSpbwZWx/rue+r5Z+PA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } + }, + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.11.tgz", + "integrity": "sha512-A5kXfGKvKWWZE0TtPrfsvT+q4Y5d1QG8gGUzpYjGydM+fARM9MuX90PrXYXe0XbsDVgyxxNzHo6giCj90bsFNw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.11.tgz", + "integrity": "sha512-z6cTycz+iJ4PVkuL4HHW4DfTfoeU/2nqYYuSOrTmH7yHK5Y0LCOnA03V4ZNxavyVaU1oOqUgIg2klN/s+USGOA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.11.tgz", + "integrity": "sha512-jShvqNtP6vDC6/A5JOAzbVV+DkgHqhl/ScVCJEbt+TUY6QYz7YnXcrg3sLtFBniro0f/Ld50ZwCWA6f7KYD1nQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.11.tgz", + "integrity": "sha512-f2i2xiNWq1Z1l2++q2fuhZRdLAT3aqxD6vRNm1RAxpUoBcdqNB3C0s1Bt+K+PbEx2F5F4gQp6hqKkphCY/xF9w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.11.tgz", + "integrity": "sha512-4Ir5FSOKIAMr4r0kExpt1s3bMgzJU3rA45AYOHtQpls0oNeqcYBKrWMlckrYH4KCfGLfkfn1tN1dmZPMVsdXow==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.11.tgz", + "integrity": "sha512-/gnRDM+39BROzAN/k1OZjDPnDMcZxB/0EUxKjONO5yVkNEvlsoMDrxGNKgZi/ttFriS2gwlDNzB65pvNbFOXIQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.11.tgz", + "integrity": "sha512-PFaK8HwvAHbaKbBcDNQihjMKYvFnA5hiENx/l5tphTDz1E0WFp32l0A7aq7lyUwGsRw/xSrNIy/gIK4thrSCrw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.11.tgz", + "integrity": "sha512-AskzJUIKRLPxkruR1wLKewGbOw+EYfU/9lOrBFj4AFrEA8hPpKFnODWNu2WLaNs0QNkEb9QIJufmVZZIL/bJlg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.11.tgz", + "integrity": "sha512-qlUGAheh2yh8afH7QBgx0PrRHN85hKnNd78x8MeMhXivuevgd8vgf6/CstOzmNKY/lLTHvNTrPy98cLnAugzJw==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.11.tgz", + "integrity": "sha512-secpEad+0vCbSfn8upFySkDskv+bGPk3THSDS9Y89yc4rb4kzqHp8Dmyd9BkQW4SnhNXBZCl/6CrO//hZahNJQ==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.11.tgz", + "integrity": "sha512-mOVBT3dPpkWm8XBWPmU4bf+U6dYDLeMo/9ojUmis4N0L5uu10qra5vOyngZ7/PSdoE4G9KvRt4bloRxNjLas7A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.11.tgz", + "integrity": "sha512-Is78i9A8Ui4SqcxUwFJ9uMmjDn58IbVTjFWYdQestFEgeuEmHMLGNriXnVJKkwG2YiZjw8cP0zCTyDMdDGtOOg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.11.tgz", + "integrity": "sha512-dUCXneZ87INUMyQ0D+C0HrEBNUPNXHaPmU5GTjyKTJEiussw9Kaj5Ln8UztPe4epV/ffvgNBEadksdYhmW6xJA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.11.tgz", + "integrity": "sha512-jByxb6qfd+bH1xUd0qnfFnb17i9sWBPY2tOavJ0l3tdr3OTu+Kvtm8cd/JV5nFt657b1VqGltxg9olOEfofXWw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.11.tgz", + "integrity": "sha512-/PzKqzAJ03i19oy2ItPvyvaVjOjBCNnfaJs8yvUdGBKmiESgnrJSQ2awd81QzFbbnAmu7YO9ZnJrDCb9VSJPRA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" + }, + "node_modules/@shikijs/core": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.4.3.tgz", + "integrity": "sha512-QCR4q2ZO/ILJEuwiBMel4wdcTDb1JGwfjKTxPDF6x8ixOaluPrVqIn06C99AcRPhmYlBR56d/Fb+GN58GzExpg==", + "license": "MIT", + "dependencies": { + "@shikijs/primitive": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5", + "hast-util-to-html": "^9.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-javascript": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.4.3.tgz", + "integrity": "sha512-FbOjFJp9VLdo1Wevs10BBtVxiTWwNLqZh5Gkhjgda/ioL15YOgeSl9n+6XMa3qRlPQzfhFNe641SrynFHYG0nQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "oniguruma-to-es": "^4.3.6" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.4.3.tgz", + "integrity": "sha512-EcOQkxdxGQrc1Row/cC2c96/v1dbZqGnEVu1qTuT/MJmp6+cXCvQussowVmCv5Tqr3KuY3c7IbM6HTW3LJ1k9w==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/langs": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.4.3.tgz", + "integrity": "sha512-ePic0yfAJGOF83D5wBHK/00EjK65oahBYxFk5epgq33WRv7X9UuxLEV8PtR0szC0z8dl7INIpIodB99JRFlR+A==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/primitive": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.4.3.tgz", + "integrity": "sha512-m0wBeLDQDeIxRdUmrCPdQqfuUamDwRL5isCfYbguKD6NiaKpVbsv+3J81DyIKgNW5h4WAIIr8T4EkgQrBBxvaQ==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/themes": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.4.3.tgz", + "integrity": "sha512-w8UHjeUnIR965KMWJHUPXOc2mNJUnK3vpVLYLvw5IYU2mnTTJ89E24OrJDBNiJDQ0qzb0tc4l7mrIXx5cFeIyw==", + "license": "MIT", + "dependencies": { + "@shikijs/types": "4.4.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/types": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.4.3.tgz", + "integrity": "sha512-UEJxmRR++MAGR6hugn0vgVS2W/6lWAts84FFSrnlH9sP0LNol7E5+NQ792pH8liWUhyMyjhTgSUH3k7iD7tc5g==", + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/braces": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/braces/-/braces-3.0.5.tgz", + "integrity": "sha512-SQFof9H+LXeWNz8wDe7oN5zu7ket0qwMu5vZubW4GCJ8Kkeh6nBWUz87+KTz/G3Kqsrp0j/W253XJb3KMEeg3w==", + "license": "MIT" + }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "license": "MIT" + }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "license": "MIT" + }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/micromatch": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-4.0.10.tgz", + "integrity": "sha512-5jOhFDElqr4DKTrTEbnW8DZ4Hz5LRUEmyrGpCMrD/NphYv3nUnaF08xmSLx1rGGnyEs/kFnhiw6dCgcDqMr5PQ==", + "license": "MIT", + "dependencies": { + "@types/braces": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, + "node_modules/@types/nlcst": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/nlcst/-/nlcst-2.0.3.tgz", + "integrity": "sha512-vSYNSDe6Ix3q+6Z7ri9lyWqgGhJTmzRjZRqyq15N0Z/1/UnVsno9G/N40NBijoYx2seFDIl0+B2mgAb9mezUCA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/node": { + "version": "24.13.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.6.tgz", + "integrity": "sha512-SGrw/h3KPFshy3OE6ZL53LMBG5vGQQ8/gIpiqz/kRZhPJ7HgwCEs8LBuNtWLa8dvGZVpSF7+Bf+c11HUrCb/yg==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/sax": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", + "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, + "node_modules/@ungap/structured-clone": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", + "license": "ISC" + }, + "node_modules/am-i-vibing": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.4.0.tgz", + "integrity": "sha512-MxT4XZL7pzLHpuvhDKdMaQHMGGkJDLluKBLsbstn+8wv9sWcFT6h+0ve9qkml95amVTZtZV83gQe2hY+ojgHLg==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/astro": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.5.tgz", + "integrity": "sha512-1p/ernaq/RuESbM5NMjWzbO6SiebMGrGcYlGjMfBhzzgM+n/X7AUmVyw8BqW2QYCdivpsHwJBcD1fqmlTZjZHg==", + "license": "MIT", + "dependencies": { + "@astrojs/compiler-rs": "^0.5.0", + "@astrojs/internal-helpers": "0.11.0", + "@astrojs/markdown-satteri": "0.4.2", + "@astrojs/telemetry": "3.3.3", + "@capsizecss/unpack": "^4.0.0", + "@clack/prompts": "^1.1.0", + "@oslojs/encoding": "^1.1.0", + "am-i-vibing": "^0.4.0", + "aria-query": "^5.3.2", + "axobject-query": "^4.1.0", + "ci-info": "^4.4.0", + "clsx": "^2.1.1", + "common-ancestor-path": "^2.0.0", + "cookie": "^2.0.1", + "devalue": "^5.8.1", + "diff": "^9.0.0", + "dset": "^3.1.4", + "es-module-lexer": "^2.0.0", + "esbuild": "^0.28.0", + "find-proc": "0.2.0", + "flattie": "^1.1.1", + "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.4", + "github-slugger": "^2.0.0", + "html-escaper": "3.0.3", + "http-cache-semantics": "^4.2.0", + "js-yaml": "^4.3.2", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.0.0", + "magicast": "^0.5.2", + "mrmime": "^2.0.1", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.0", + "p-queue": "^9.1.0", + "package-manager-detector": "^1.6.0", + "piccolore": "^0.1.3", + "picomatch": "^4.0.4", + "shiki": "^4.0.2", + "smol-toml": "^1.8.0", + "svgo": "^4.1.0", + "tinyclip": "^1.0.1", + "tinyexec": "^1.0.4", + "tinyglobby": "^0.2.15", + "ultrahtml": "^1.6.0", + "unifont": "~0.7.5", + "unstorage": "^1.17.5", + "verkit": "^0.4.0", + "vite": "^8.0.13", + "vitefu": "^1.1.2", + "xxhash-wasm": "^1.1.0", + "yargs-parser": "^22.0.0", + "zod": "^4.5.4" + }, + "bin": { + "astro": "bin/astro.mjs" + }, + "engines": { + "node": ">=22.12.0", + "npm": ">=9.6.5", + "pnpm": ">=7.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/astrodotbuild" + }, + "optionalDependencies": { + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/astro-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", + "license": "MIT", + "dependencies": { + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" + }, + "peerDependencies": { + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/bcp-47-match": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/bcp-47-match/-/bcp-47-match-2.0.3.tgz", + "integrity": "sha512-JtTezzbAibu8G0R9op9zb3vcWZd9JF6M0xOYGPn0fNCd7wOpRB1mU2mH9T8gaBGbAAyIIVgB2G7xG0GP98zMAQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/chokidar": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", + "integrity": "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==", + "license": "MIT", + "dependencies": { + "readdirp": "^5.0.0" + }, + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/common-ancestor-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/common-ancestor-path/-/common-ancestor-path-2.0.0.tgz", + "integrity": "sha512-dnN3ibLeoRf2HNC+OlCiNc5d2zxbLJXOtiZUudNFSXZrNSydxcCsSpRzXwfu7BBWCIfHPw+xTayeBvJCP/D8Ng==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/cookie": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", + "license": "MIT", + "engines": { + "node": ">=22" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cookie-es": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", + "license": "MIT" + }, + "node_modules/crossws": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", + "integrity": "sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==", + "license": "MIT", + "dependencies": { + "uncrypto": "^0.1.3" + } + }, + "node_modules/css-select": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-selector-parser": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/css-selector-parser/-/css-selector-parser-3.3.0.tgz", + "integrity": "sha512-Y2asgMGFqJKF4fq4xHDSlFYIkeVfRsm69lQC1q9kbEsH5XtnINTMrweLkjYMeaUgiXBy/uvKeO/a1JHTNnmB2g==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "license": "MIT" + }, + "node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/devalue": { + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", + "license": "MIT" + }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/direction": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/direction/-/direction-2.0.1.tgz", + "integrity": "sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==", + "license": "MIT", + "bin": { + "direction": "cli.js" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/dom-serializer/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dset": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/dset/-/dset-3.1.4.tgz", + "integrity": "sha512-2QF/g9/zTaPDc3BjNcVTGoBbXBgYfMTTceLaYcFJ/W9kggFUkhxD/hMEeuLKbugyef9SqAx8cpgwlIP/jinUTA==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-module-lexer": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.3.2.tgz", + "integrity": "sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==", + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.4.tgz", + "integrity": "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==", + "license": "MIT" + }, + "node_modules/expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", + "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", + "license": "MIT", + "dependencies": { + "@expressive-code/core": "^0.44.2", + "@expressive-code/plugin-frames": "^0.44.2", + "@expressive-code/plugin-shiki": "^0.44.2", + "@expressive-code/plugin-text-markers": "^0.44.2" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-proc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", + "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/flattie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", + "integrity": "sha512-9UbaD6XdAL97+k/n+N7JwX46K/M6Zc6KcFYskrYL8wbBV/Uyk0CTAMY0VT+qiK5PM7AIc9aTWYtq65U7T+aCNQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/fontace": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/fontace/-/fontace-0.4.1.tgz", + "integrity": "sha512-lDMvbAzSnHmbYMTEld5qdtvNH2/pWpICOqpean9IgC7vUbUJc3k+k5Dokp85CegamqQpFbXf0rAVkbzpyTA8aw==", + "license": "MIT", + "dependencies": { + "fontkitten": "^1.0.2" + } + }, + "node_modules/fontkitten": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/fontkitten/-/fontkitten-1.0.3.tgz", + "integrity": "sha512-Wp1zXWPVUPBmfoa3Cqc9ctaKuzKAV6uLstRqlR56kSjplf5uAce+qeyYym7F+PHbGTk+tCEdkCW6RD7DX/gBZw==", + "license": "MIT", + "dependencies": { + "tiny-inflate": "^1.0.3" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.4", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.4.tgz", + "integrity": "sha512-7nF7C9fIPFEMHgEMEfgIlO9wDdZ8CyHw27rWciFZfHvHDReIiPhsYuzPRXsfvBCqFy1l8RRyyWV7QLM+ZhUJsQ==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/github-slugger": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", + "integrity": "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==", + "license": "ISC" + }, + "node_modules/h3": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", + "license": "MIT", + "dependencies": { + "cookie-es": "^1.2.3", + "crossws": "^0.3.5", + "defu": "^6.1.6", + "destr": "^2.0.5", + "iron-webcrypto": "^1.2.1", + "node-mock-http": "^1.0.4", + "radix3": "^1.1.2", + "ufo": "^1.6.3", + "uncrypto": "^0.1.3" + } + }, + "node_modules/hast-util-embedded": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", + "integrity": "sha512-naH8sld4Pe2ep03qqULEtvYr7EjrLK2QHY8KJR6RJkTUjPGObe1vnx585uzem2hGra+s1q08DZZpfgDVYRbaXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-format": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/hast-util-format/-/hast-util-format-1.1.0.tgz", + "integrity": "sha512-yY1UDz6bC9rDvCWHpx12aIBGRG7krurX0p0Fm6pT547LwDIZZiNr8a+IHDogorAdreULSEzP82Nlv5SZkHZcjA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "html-whitespace-sensitive-tag-names": "^3.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-html": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", + "integrity": "sha512-CUSRHXyKjzHov8yKsQjGOElXy/3EKpyX56ELnkHH34vDVw1N1XSQ1ZcAvTyAPtGqLTuKP/uxM+aLkSPqF/EtMw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "devlop": "^1.1.0", + "hast-util-from-parse5": "^8.0.0", + "parse5": "^7.0.0", + "vfile": "^6.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-from-parse5": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", + "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "devlop": "^1.0.0", + "hastscript": "^9.0.0", + "property-information": "^7.0.0", + "vfile": "^6.0.0", + "vfile-location": "^5.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-has-property": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-has-property/-/hast-util-has-property-3.0.0.tgz", + "integrity": "sha512-MNilsvEKLFpV604hwfhVStK0usFY/QmM5zX16bo7EjnAEGofr5YyI37kzopBlZJkHD4t887i+q/C8/tr5Q94cA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-body-ok-link": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-is-body-ok-link/-/hast-util-is-body-ok-link-3.0.1.tgz", + "integrity": "sha512-0qpnzOBLztXHbHQenVB8uNuxTnm/QBFUOmdOSsEn7GnBtyY07+ENTWVFBAnXd/zEgd9/SUG3lRY7hSIBWRgGpQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-is-element": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-is-element/-/hast-util-is-element-3.0.0.tgz", + "integrity": "sha512-Val9mnv2IWpLbNPqc/pUem+a7Ipj2aHacCwgNfTiK0vJKl0LF+4Ba4+v1oPHFpf3bLYmreq0/l3Gud9S5OH42g==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-minify-whitespace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hast-util-minify-whitespace/-/hast-util-minify-whitespace-1.0.1.tgz", + "integrity": "sha512-L96fPOVpnclQE0xzdWb/D12VT5FabA7SnZOUMtL1DbXmYiHJMXZvFkIZfiMmTCNJHUeO2K9UYNXoVyfz+QHuOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-parse-selector": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", + "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-phrasing": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-phrasing/-/hast-util-phrasing-3.0.1.tgz", + "integrity": "sha512-6h60VfI3uBQUxHqTyMymMZnEbNl1XmEGtOxxKYL7stY2o601COo62AWAYBQR9lZbYXYSBoxag8UpPRXK+9fqSQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-embedded": "^3.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-is-body-ok-link": "^3.0.0", + "hast-util-is-element": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-select": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/hast-util-select/-/hast-util-select-6.0.4.tgz", + "integrity": "sha512-RqGS1ZgI0MwxLaKLDxjprynNzINEkRHY2i8ln4DDjgv9ZhcYVIHN9rlpiYsqtFwrgpYU361SyWDQcGNIBVu3lw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "bcp-47-match": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "css-selector-parser": "^3.0.0", + "devlop": "^1.0.0", + "direction": "^2.0.0", + "hast-util-has-property": "^3.0.0", + "hast-util-to-string": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "nth-check": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-html": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.5.tgz", + "integrity": "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-whitespace": "^3.0.0", + "html-void-elements": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "stringify-entities": "^4.0.0", + "zwitch": "^2.0.4" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-mdast": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/hast-util-to-mdast/-/hast-util-to-mdast-10.1.2.tgz", + "integrity": "sha512-FiCRI7NmOvM4y+f5w32jPRzcxDIz+PUqDwEqn1A+1q2cdp3B8Gx7aVrXORdOKjMNDQsD1ogOr896+0jJHW1EFQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "hast-util-phrasing": "^3.0.0", + "hast-util-to-html": "^9.0.0", + "hast-util-to-text": "^4.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "mdast-util-to-string": "^4.0.0", + "rehype-minify-whitespace": "^6.0.0", + "trim-trailing-lines": "^2.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-string": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/hast-util-to-string/-/hast-util-to-string-3.0.1.tgz", + "integrity": "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-to-text": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/hast-util-to-text/-/hast-util-to-text-4.0.2.tgz", + "integrity": "sha512-KK6y/BN8lbaq654j7JgBydev7wuNMcID54lkRav1P0CaE1e47P72AWWPiGKXTJU271ooYzcvTAn/Zt0REnvc7A==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "hast-util-is-element": "^3.0.0", + "unist-util-find-after": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hastscript": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", + "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "hast-util-parse-selector": "^4.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-escaper": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", + "integrity": "sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==", + "license": "MIT" + }, + "node_modules/html-void-elements": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", + "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/html-whitespace-sensitive-tag-names": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-whitespace-sensitive-tag-names/-/html-whitespace-sensitive-tag-names-3.0.1.tgz", + "integrity": "sha512-q+310vW8zmymYHALr1da4HyXUQ0zgiIwIicEfotYPWGN0OJVEN/58IJ3A4GBYcEq3LGAZqKb+ugvP0GNB9CEAA==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "license": "BSD-2-Clause" + }, + "node_modules/i18next": { + "version": "26.4.2", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.2.tgz", + "integrity": "sha512-RX+R0VLg13IbvRuJSxnqykUFS9vQZTl8wYpWPCIUDWVrSGjsQywB5Y+pjzrkboxGAuYfJZVH1InFTdgBdxq6ug==", + "funding": [ + { + "type": "individual", + "url": "https://www.locize.com/i18next" + }, + { + "type": "individual", + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + }, + { + "type": "individual", + "url": "https://www.locize.com" + } + ], + "license": "MIT", + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/iron-webcrypto": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", + "integrity": "sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/brc-dd" + } + }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-docker": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/lru-cache": { + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/magic-string": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.2.tgz", + "integrity": "sha512-vG+rjFRj1PqdIBozIxAGMjPlOhaVe+GXpbttY/iSK7rGcJRMlwNJO7dcUwmUqkymsFLJiNGI06t4D7Fr7yRC9g==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.6.0" + } + }, + "node_modules/magicast": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "source-map-js": "^1.2.1" + } + }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-directive": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", + "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "license": "CC0-1.0" + }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "parse-entities": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mrmime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.19", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.19.tgz", + "integrity": "sha512-Y2tUNy4ouw6tq5oDSKeQYGOyhkUBhNOcGV/02KC+6kd9eDGqdZd++mjMiIDilrBYvjEnCYvVtsuHCuP+okSfug==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neotraverse": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/nlcst-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/nlcst-to-string/-/nlcst-to-string-4.0.0.tgz", + "integrity": "sha512-YKLBCcUYKAg0FNlOBT6aI91qFmSiFKiluk655WzPF+DDMA02qIyy8uiRqI8QXtcFpEvll12LpL5MXqEmAZ+dcA==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, + "node_modules/node-mock-http": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/obug": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/ofetch": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/ofetch/-/ofetch-1.5.1.tgz", + "integrity": "sha512-2W4oUZlVaqAPAil6FUg/difl6YhqhUR7x2eZY4bQCko22UXg3hptq9KLQdqFClV+Wu85UX7hNtdGTngi/1BxcA==", + "license": "MIT", + "dependencies": { + "destr": "^2.0.5", + "node-fetch-native": "^1.6.7", + "ufo": "^1.6.1" + } + }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "node_modules/oniguruma-parser": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", + "license": "MIT" + }, + "node_modules/oniguruma-to-es": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", + "license": "MIT", + "dependencies": { + "oniguruma-parser": "^0.12.2", + "regex": "^6.1.0", + "regex-recursion": "^6.0.2" + } + }, + "node_modules/p-limit": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.2.1" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue": { + "version": "9.3.3", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.3.tgz", + "integrity": "sha512-NXAOdnEe5FsZJfT4oK84lE1Y5cFFdWlRuOo5tww8DyNMxyRXwn39fIkUtNLKppcPC+UYU/bXujNCUGDv01y7CA==", + "license": "MIT", + "dependencies": { + "eventemitter3": "^5.0.4", + "p-timeout": "^7.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-timeout": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.8.0.tgz", + "integrity": "sha512-yQA4H19AmPEoMUeavPMDIe1higySl/gH/yaQrkT/s07Qp+7pp2hYz30N3z2l5BkjVkF9Ow6o0wjJamm2y7Sn0A==", + "license": "MIT" + }, + "node_modules/pagefind": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", + "license": "MIT", + "bin": { + "pagefind": "lib/runner/bin.cjs" + }, + "optionalDependencies": { + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" + } + }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/piccolore": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", + "integrity": "sha512-o8bTeDWjE086iwKrROaDf31K0qC/BENdm15/uH9usSC/uZjJOKb2YGiVHfLY4GhwsERiPI1jmwI2XrA7ACOxVw==", + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.28", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.28.tgz", + "integrity": "sha512-RRuzqDtt5Y9h3quz5hWhK+TPnsmVs6WwSU6LkJMeY4HstUEDuYTG8UJSdawMRzmzAtV+KEoG8N3Qg2qLy5vM/A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.18", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", + "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^6.1.1" + }, + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prismjs": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", + "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/property-information": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/radix3": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/radix3/-/radix3-1.1.2.tgz", + "integrity": "sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==", + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", + "license": "MIT", + "engines": { + "node": ">= 20.19.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/regex/-/regex-6.1.0.tgz", + "integrity": "sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-recursion": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-6.0.2.tgz", + "integrity": "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==", + "license": "MIT", + "dependencies": { + "regex-utilities": "^2.3.0" + } + }, + "node_modules/regex-utilities": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-utilities/-/regex-utilities-2.3.0.tgz", + "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", + "license": "MIT" + }, + "node_modules/rehype-expressive-code": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", + "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", + "license": "MIT", + "dependencies": { + "expressive-code": "^0.44.2" + } + }, + "node_modules/rehype-minify-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/rehype-minify-whitespace/-/rehype-minify-whitespace-6.0.2.tgz", + "integrity": "sha512-Zk0pyQ06A3Lyxhe9vGtOtzz3Z0+qZ5+7icZ/PL/2x1SHPbKao5oB/g/rlc6BCTajqBb33JcOe71Ye1oFsuYbnw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-minify-whitespace": "^1.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-parse": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", + "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rehype-remark": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/rehype-remark/-/rehype-remark-10.0.1.tgz", + "integrity": "sha512-EmDndlb5NVwXGfUa4c9GPK+lXeItTilLhE6ADSaQuHr4JUlKw9MidzGzx4HpqZrNCt6vnHmEifXQiiA+CEnjYQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "hast-util-to-mdast": "^10.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-directive": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retext-smartypants": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/retext-smartypants/-/retext-smartypants-6.2.0.tgz", + "integrity": "sha512-kk0jOU7+zGv//kfjXEBjdIryL1Acl4i9XNkHxtM7Tm5lFiCog576fjNC9hjoR7LTKQ0DsPWy09JummSsH1uqfQ==", + "license": "MIT", + "dependencies": { + "@types/nlcst": "^2.0.0", + "nlcst-to-string": "^4.0.0", + "unist-util-visit": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/rolldown": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.11.tgz", + "integrity": "sha512-qpSwIyz0jHQq5qXBTNxFmE6664rJ7O+4TvPFOiOaBSrz8IOHc1koKKSqTM2H6u1UG1+TveuC6vaDHKXFOvb1Kw==", + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.151.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.11", + "@rolldown/binding-android-arm64": "1.2.11", + "@rolldown/binding-darwin-arm64": "1.2.11", + "@rolldown/binding-darwin-x64": "1.2.11", + "@rolldown/binding-freebsd-x64": "1.2.11", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.11", + "@rolldown/binding-linux-arm64-gnu": "1.2.11", + "@rolldown/binding-linux-arm64-musl": "1.2.11", + "@rolldown/binding-linux-ppc64-gnu": "1.2.11", + "@rolldown/binding-linux-s390x-gnu": "1.2.11", + "@rolldown/binding-linux-x64-gnu": "1.2.11", + "@rolldown/binding-linux-x64-musl": "1.2.11", + "@rolldown/binding-openharmony-arm64": "1.2.11", + "@rolldown/binding-win32-arm64-msvc": "1.2.11", + "@rolldown/binding-win32-x64-msvc": "1.2.11" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" + }, + "optionalDependencies": { + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" + } + }, + "node_modules/sax": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" + } + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shiki": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.4.3.tgz", + "integrity": "sha512-Mb/GvXPHBAXdgGIcnfU5L3ldpn1XcxrGkPHwqgRx17/I2XRfqlFKk2vGkHWINn1kdXvzJZeuO3is6I9KLPFm0g==", + "license": "MIT", + "dependencies": { + "@shikijs/core": "4.4.3", + "@shikijs/engine-javascript": "4.4.3", + "@shikijs/engine-oniguruma": "4.4.3", + "@shikijs/langs": "4.4.3", + "@shikijs/themes": "4.4.3", + "@shikijs/types": "4.4.3", + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.5" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "license": "MIT" + }, + "node_modules/sitemap": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-9.0.1.tgz", + "integrity": "sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==", + "license": "MIT", + "dependencies": { + "@types/node": "^24.9.2", + "@types/sax": "^1.2.1", + "arg": "^5.0.0", + "sax": "^1.4.1" + }, + "bin": { + "sitemap": "dist/esm/cli.js" + }, + "engines": { + "node": ">=20.19.5", + "npm": ">=10.8.2" + } + }, + "node_modules/smol-toml": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.0.tgz", + "integrity": "sha512-hpd+HLON7HdZXqYchMM/+LaTTbdK0AU3NngIJ4KVyWbY9bfQqdL9cD+4yf6dUoU2Ap4VsU0JkQi6FxAI1B2mXQ==", + "license": "BSD-3-Clause", + "engines": { + "node": ">= 18" + }, + "funding": { + "url": "https://github.com/sponsors/cyyynthia" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/starlight-llms-txt": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/starlight-llms-txt/-/starlight-llms-txt-0.12.0.tgz", + "integrity": "sha512-ya4c0ZoiPUXuHDzd3AYCKh78Y3II3EToSE9Yp6uVN0c2UUKoNpqTjijhW86W/4toZzSUhWSnxeYs7QBcXNIPGg==", + "license": "MIT", + "dependencies": { + "@astrojs/mdx": "^8.0.1", + "@types/hast": "^3.0.5", + "@types/micromatch": "^4.0.10", + "github-slugger": "^2.0.0", + "hast-util-select": "^6.0.4", + "micromatch": "^4.0.8", + "rehype-parse": "^9.0.1", + "rehype-remark": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-stringify": "^11.0.0", + "unified": "^11.0.5", + "unist-util-remove": "^4.0.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.41.0", + "astro": "^7.0.0" + } + }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/svgo": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", + "license": "MIT", + "dependencies": { + "commander": "^11.1.0", + "css-select": "^6.0.0", + "css-tree": "^3.0.1", + "css-what": "^7.0.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "1.6.1" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, + "node_modules/tinyclip": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", + "license": "MIT", + "engines": { + "node": "^16.14.0 || >= 17.3.0" + } + }, + "node_modules/tinyexec": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trim-trailing-lines": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-2.1.0.tgz", + "integrity": "sha512-5UR5Biq4VlVOtzqkm2AZlgvSlDJtME46uV0br0gENbwN4l5+mMKT4b9gJKqWtuL2zAIqajGJGuvbCbcAJUZqBg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD", + "optional": true + }, + "node_modules/ufo": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", + "license": "MIT" + }, + "node_modules/ultrahtml": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", + "license": "MIT" + }, + "node_modules/uncrypto": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/uncrypto/-/uncrypto-0.1.3.tgz", + "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "license": "MIT" + }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unifont": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", + "license": "MIT", + "dependencies": { + "css-tree": "^3.1.0", + "ohash": "^2.0.11", + "undici": "^8.0.0" + } + }, + "node_modules/unist-util-find-after": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-find-after/-/unist-util-find-after-5.0.0.tgz", + "integrity": "sha512-amQa0Ep2m6hE2g72AugUItjbuM8X8cGQnFoHk0pGfrFeT9GZhzN5SW8nRsiGKK7Aif4CrACPENkA6P/Lw6fHGQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-remove": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-remove/-/unist-util-remove-4.0.0.tgz", + "integrity": "sha512-b4gokeGId57UVRX/eVKej5gXqGlc9+trkORhFJpu9raqZkZhU0zm8Doi05+HaiBsMEIJowL+2WtQ5ItjsngPXg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unstorage": { + "version": "1.17.5", + "resolved": "https://registry.npmjs.org/unstorage/-/unstorage-1.17.5.tgz", + "integrity": "sha512-0i3iqvRfx29hkNntHyQvJTpf5W9dQ9ZadSoRU8+xVlhVtT7jAX57fazYO9EHvcRCfBCyi5YRya7XCDOsbTgkPg==", + "license": "MIT", + "dependencies": { + "anymatch": "^3.1.3", + "chokidar": "^5.0.0", + "destr": "^2.0.5", + "h3": "^1.15.10", + "lru-cache": "^11.2.7", + "node-fetch-native": "^1.6.7", + "ofetch": "^1.5.1", + "ufo": "^1.6.3" + }, + "peerDependencies": { + "@azure/app-configuration": "^1.8.0", + "@azure/cosmos": "^4.2.0", + "@azure/data-tables": "^13.3.0", + "@azure/identity": "^4.6.0", + "@azure/keyvault-secrets": "^4.9.0", + "@azure/storage-blob": "^12.26.0", + "@capacitor/preferences": "^6 || ^7 || ^8", + "@deno/kv": ">=0.9.0", + "@netlify/blobs": "^6.5.0 || ^7.0.0 || ^8.1.0 || ^9.0.0 || ^10.0.0", + "@planetscale/database": "^1.19.0", + "@upstash/redis": "^1.34.3", + "@vercel/blob": ">=0.27.1", + "@vercel/functions": "^2.2.12 || ^3.0.0", + "@vercel/kv": "^1 || ^2 || ^3", + "aws4fetch": "^1.0.20", + "db0": ">=0.2.1", + "idb-keyval": "^6.2.1", + "ioredis": "^5.4.2", + "uploadthing": "^7.4.4" + }, + "peerDependenciesMeta": { + "@azure/app-configuration": { + "optional": true + }, + "@azure/cosmos": { + "optional": true + }, + "@azure/data-tables": { + "optional": true + }, + "@azure/identity": { + "optional": true + }, + "@azure/keyvault-secrets": { + "optional": true + }, + "@azure/storage-blob": { + "optional": true + }, + "@capacitor/preferences": { + "optional": true + }, + "@deno/kv": { + "optional": true + }, + "@netlify/blobs": { + "optional": true + }, + "@planetscale/database": { + "optional": true + }, + "@upstash/redis": { + "optional": true + }, + "@vercel/blob": { + "optional": true + }, + "@vercel/functions": { + "optional": true + }, + "@vercel/kv": { + "optional": true + }, + "aws4fetch": { + "optional": true + }, + "db0": { + "optional": true + }, + "idb-keyval": { + "optional": true + }, + "ioredis": { + "optional": true + }, + "uploadthing": { + "optional": true + } + } + }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "license": "MIT" + }, + "node_modules/verkit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.4.1.tgz", + "integrity": "sha512-mMVzj0TXExtVdlDEq+Mzp0eyOuyznNpFobNM3uAqe3HVm/Ps2c+u17BligMkZjZCA6EiXpoon8iRTer3iu40SQ==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-location": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", + "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vite": { + "version": "8.3.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.1.tgz", + "integrity": "sha512-/bvH9E9tmCXRGp2uXY3WbOldqpTwFkbha/8ANaEQ6VkxhH60KyqLwgZq6lG2y+4uT55x9+9eUHMpQ7uGnOCKjA==", + "license": "MIT", + "dependencies": { + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.28", + "rolldown": "~1.2.9", + "tinyglobby": "^0.2.17" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitefu": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", + "license": "MIT", + "workspaces": [ + "tests/deps/*", + "tests/projects/*", + "tests/projects/workspace/packages/*" + ], + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, + "node_modules/web-namespaces": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", + "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/xxhash-wasm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", + "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", + "license": "MIT" + }, + "node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.6.5", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.5.tgz", + "integrity": "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + } + } +} diff --git a/website/package.json b/website/package.json new file mode 100644 index 0000000..6516925 --- /dev/null +++ b/website/package.json @@ -0,0 +1,20 @@ +{ + "name": "sqlclean-website", + "private": true, + "type": "module", + "engines": { + "node": ">=22.12.0" + }, + "scripts": { + "dev": "astro dev", + "build": "astro build", + "preview": "astro preview" + }, + "dependencies": { + "@astrojs/starlight": "^0.42.4", + "@lucide/astro": "^1.48.0", + "astro": "^7.3.5", + "sharp": "^0.35.4", + "starlight-llms-txt": "^0.12.0" + } +} diff --git a/website/public/assets/icon.png b/website/public/assets/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..48c19ba72b07d2a8e7b54223f13e6e91869577dc GIT binary patch literal 12948 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 ZjGyMV)hcwyf1VA1jD(_ittc??{{Ri?`+xud literal 0 HcmV?d00001 diff --git a/website/public/assets/logo-dark.png b/website/public/assets/logo-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..9fecb46a0ae81ceb912e2c91472e7d09904cbc20 GIT binary patch literal 19315 zcmZ^~1xzJQ4>*XsJ1p+*?yigbg9i^-+!uFucXxMp_r=|PfyLeZ|NC;eOD>noB%Mj8 zP20&#GwpODRFq_p5eN{#z`&5@WF^(Wz`*JL8;ioh{0F761B(CaNKItb6v4neslmYf zgTcVw{zLhnfPuNNf`OfZz`*#^!N71GvO84;|C@j@R*;bd`~F|c?i-vmt^F@cGmrm&hDS1fwEq}r{~rm?mbRu&&PKKl|6d*sw*O1W;+NVo7?`}e zoTQkB`^IICTeg8_%X6P~-o4=L!$U9{1_Wg@7=kf0cvs=7wrYd%*iL=^j`hIro>pt^ zhm8@0$jEY1ZY->1didan+<1PDjJsr~mFv%g?6T>ftjVZ4W+5C?g0q%hmo2LXUNyUb z%@PpAtRbTPRZ zIvN;kXn;{f4~zu+rSt`ut67*U{JHYfGmdpCh%VBEGU0D~jGAe(=+C(6_P%v4xugdc z1=m@7$Lj=25Zao&=`XU8SPY!TYYeK9a0`@CG8h@sSTMa{Hq>uc%n9G&FGfcS8bSK*n!CK<5%P(1fr4U#ipGIz;~!Z#v-_m|Lf}zwX6|PkjBAin7wG*F{i^-?`C&g>n%hKUlsB>GUs_wM zDxREbE|I|CNvYPr>ZL63Z)k>>?K*|Xk>CRev5d$@7<>EH`Tu8x*mGB6V%ZWSXksdF z*qVq;5|#79d5BoKH2rO~yW;AwuA#=7p2%8N_R*eN&CG?1Cm~RM(_R32b@G8jqEAUv z4_ozt02?FHSr@x$<~?iVf9ftd_t8~kN&@f;G|mQr#c~8Pj?PxBUeKdOqnB#o}aBTsC1WD5Q3XCyU>K)q`X8}Hn@1>tY_J*sm#|EEu*n=V=4~mQgZ25u^ zIor=&^Z(h&Dfg~Wn>AO(A=6RjTioT#G=9V+viwr@s%$I6(2~dT^bzrGIrTZ3Ti4ph zH6TReDGsyQ}}VhJ;h<=NH5N7^Ie686~V{@LVgNmfp-m)tVvYN5}4APdP!nF&}-c zPS=7MYq0}XJ_^?Q$zr9VW=IH+Cg52D5%PJ(a-9Osd3tIn&fC58?-a{*=&O@#!Hd0w zKkQq6Ayg_6+&9@N#0Uko&W_O$#t8LoBd%0dj{T5Ez3`rJcsLi zZZQV*0jcu^o8c{ifm(sJU8r8x|Czm;oQy>FXwE?SJT~6aNYz*-t#4}sr7Zu9ewsyF zNOP)0?u{w)2rvmVq42;Q!2n(f4|K^dTyK3k$T9jcSR_4ViO8^EbF>5&dE+On%vF>C z8|?WwuX(5T$oepwFlzp%yvS5O)(ZxD% zogL}?Y_Q0sWn|vvQ*OrM zjaEN_SwUB)buRb@6J{wKgD}M#Z2rbsr&L_%gV+|)t;fKM0y&OhD(8PxS%-!Hy|M-6 zVSP#L!Gpe0F9y=5i6JHM;`qa;IYpNePNuOA1%9kvvId*E8vX;a*Iy7LOPMwcjs*ya z`VG7^L`?>+6Ga|eP%DWIK&a?Co+tYB4p#r75NHn(*1DC8Cbn{d-A@^uK`K`c1}~Tw zu46q@xK3lv`fscBoe0k^>vU=mdW2(kLQv;2Xw*=8`EIGEU!}~WN9F+W;@vwKg8!<9 z<`}OPiH-#j`O|J&V%1@H&Jny?3)P+4v~?xpCgpHL*np?XzX>^%9JYm_#n8*ia#vFd zKhTy(|7rNtFA{GJI_s?f_G0S&efq28RJg6URu@7c{7>Km zV?D^83A>cZ@ik382k6F2XIT>{6@p%Wj@-4=^EQH37^K1ra6lTJ4`qV*7g)OIY>c%* z3T?tv&MidQc(Kc7*nD0^kRiSiv<6X~tb@=wGIx2kfm2wXjqND6PsaA-FH`8|JCc0% zN9DqkrgRbc6TCn2GV;cvW=~6+6+%FQ1*m}=3o@r!OiNbjR$DWCk?xh^HxvW{ zPgJ78xM`1XL*A0=0F-Jd7Ic~Cc~_-Ok`7UJr>A|W{>0K03qVV|YH|BAFY2l;E}jLG z8TeCI(2Wxb)Vbyv&2GY)5beuvgI(yM45^^p5&PIyR#6m|zYP%$l})!?>zcl}P3)zA zm~5YGK^7}a7HY})$?%f|TFba~VOW`AD9EuuCEGY;0SUs%A2J{ai3p7QM|$8%(g|mg zB+O)FIb_C^T9LtFEPXxh3G5?FEw{?BdkR#BUStO?sOr@oGT` zWzT|`(#8d{sMvbY?tnY#nnB|+tIvuRSm_lpd9EuFWF=@tCZga9;;2kB+z{ck&>u=` za)cL-9F77l?jwQgadX=wahR*kkM0~wBm)%C4rci`!Su0{|6Zd=gY~bxwC91{b;=4GQ+mp=ptBOj2!Y1Cd+G%2N}SM9wV z_i{HhyXVs7X53`f(nf|3DHo{ktaLgq5?m-=YKV;B2zGW|)*+y#vn6XV+Hcm1;9Uq?-uPFuw3{}1NnpH-N9GdV z=au2f=lvx9=bEka)326YQYWuAVX6N9G?D&S^S&%Mi1r=%`nZ3F1S;rnKSq_&`efzo zTIHzf>y7ZH)u=y3J;0pM@y%w_YiEek6p-ohmNi5C!NUJbBJBqbUZ0d#j zoBJnRME<^nj5&C3kkm$*2@mh5(CshzW8XhpDU6~Q#OLouC8Xh1MiQ(+Ua6Lq$-~Q$ zjzkoSo>`j1?elVYQT}z`1A!3GjF$YjWzV7ghA#L!Sdhssq*Z>%!6l%(TnySM-Q%-( zvKwt%O~_1_yu9xIl0uU6g4hq}r|WkF!6q#uApXJ}C63_2;{nqyGw9+%37OYG)8#HI z>4Kr1$&O?eMD^squ^`@09?M_YlF7Io-m>K7c4~roqh9;Hw)2$hmJ%A)wy2pcRhdj1 zTVAn%u6Iit6o%*(j~@oMcz1jyWi$0sY8`Bc5d4*FGkQqD5r+VuVaCy>wQT5H9Ek#ZaidC?#Mm! zuUy7&eo;+{mp^9R{DEcgmVYTD4svs{=&4jU?)=75wiz7976}Z9N*mp1#{0o?lW#dK z^{?-$dpl?nygdE8L`JuF8D5XBzM3zOg%&C?a?dC`w8lR_EfZ%Ff+rxhX9TyE=2Y^T}0G27^heC7HaU!$z#Od}~5_JvN8SVT#D zjdoakGxZ^v98#+=mCIMC-KToZlic7~`hjX$*8ZS#H1h3o@-L>wt-0e`~*; zR;$!ri_}V@gyhW1x%I2{_%A1x5(LyD8AhC>4z6C^*10kY7kH(*y@p6`n^*R8XOxZ< z^uH)O@+h9||F{(CLq3A<)s~g)`s!7)^ij)iqdGm0X@yoLC|p1b-%Zxd7ibceVLP^I z3Ag4ii^Q~;`G{F_W3|`;KWQz2wZ$tn3>KH2fh)C9Ev`+LLz{}{be2a#XJhg%;g#1T z)=HV4ar*XY*5|xn)+X_7&f#8@WODLU+lN;ya!FQcf3QKxOtRMft%mw`mWjeYYom?` zIxtI?0%e`Jz#7+IgP46bQ;2CI08&eo4?W-FeQzWW1!)^6ZGEO!Z!B^xjwc-#$KeXf zmCu*J5=!)z0`!STIo?O9tGC3mjmvSok!JgJha73(TSHe~x&G6K49Vmn+oL+wWh%q; zY!AIlUV}*&t@f8s0nK}xXr2@6Z7$Qlqhbr95FP}jS;%{*=?mag_Ur0^wxNT>>%#RW zV^dn@?E}30JbV#mKdSzr>Uo=qI*4GtO&ZUhpLKh0z(30ds!2%FCagB5UwTe@SPR6n z<{wQKM3zr3%ak-H*fL7m)Q*_1gefE%2Z}V}5OXRea=!D9hVB=G$;e~y`+mDq+Y6=t z^Bj8Y$GQK0T`#ig+w)fZ_drV~q`G?7L6}y)n%{x%c0J!*;sh}sko&i6OHqisj>a&n zRJwEfbmS^PL`oYd-ROaQn{6UDt)NQRjd?IFYTY~I-Y#uT*^HtW#6NWaZsv+2+4tzy z<1PWM@$%Xxo9BRYx|=%IluGER6@DQI5@Xl}*l$UjJ^qoYVtgPFBpJLFx39A`yuTk5 z3yV1RMq~D%IV$Vc|Dk}|CS`JDi9|mGAaT@wS=m(vRH0q3DQ*d&b||%i@}QJ7yi`Z| z2~8o_VE`dISPr#GOjt#ZRd&S)sxn3eV(s7Hxbk{rkf!|E@DC(FMS=H}A3mHulf zuJgc3M3NId5%lv~M;?UJ)Qy9xdwPqFXpX;6hE5;4^LM9oZ)_@l*&Mm*6Sb}Ejb2hS z3*R$F_=>^t6=vhr@FZW3nZ`{-5@TyXv1h^YEgag)(47Uu9K!D&;P|Ji^=HZ` zukBiJgL&U8t47;}Db>t2h1|_5!`?WHSjzEigZTb({C!#P7y86y47F>uqD)0@bPY^C z_7~~xbgW0yhCn|)WS{+mi;ffay&KklnQSS4u3#t~9KZ}XAc66sg(EoYGyx`4cZGuWnkSfvz4+p5 z2|gOvRFKt37i>)>6^V!xQ3i0X%(|SNUp`K%^RlXCrsSN8boK>0ncTT@3M|2jKR~zW zNY6w;ig{ve;RN{U?NPN|Hhe3V8b$t%0x6SgGe%&4UlFfXJ^_|(uI7)2Xuut@f<6Y! zwo}Z$b6%&&=#?;#i*RpWP8eUJT0%CNdBU4sI$ByKH*V`VsTFQnc5a8JdT zE-6%B!ea56+wg;XkZ$Xy)jQ%PgfHDGb#&tZXY@!ve82Ipl8f zjir`l38pm`;=9KnPuw%3sPjBVfC>4e;{-~LVpSS_Qyn!QTd0e^itKgIuN2d zjmEt}=;K%0!K^6>wLoXA(rbL6@3grHgU^kvR4?%O7kOdo$Sgab4w)Jr2Cl_L1)F>FG0PM=-~Dg zOX-|O{#Tr{R>)0$g{{VP(9|naT=gH6u-<}AKn-@nT0-jDH_b@JQP zuWM94zq2OuenmId908pZ23>R%AGMf4KLnJBvr2Hp)U0Ht%%+BXOO^*+zha1&R)1s& zUV*2wt|u`r?n_l(2f(cu3{iXZoPN`xMuMY(?eQoJt~qo0p|GHpK?DNMH2OX#hZpf^ z1qLqF(SQh!uWPB}6obq1gDSDJ#5#ZBAY#A)S}b}!8^*PV#m5vi&MT+K{Nfr_g0aszyM&qr?J@7>l$6h5Y?ZyE*onso6ng3+dL1cs->7s ze@bBAa$&||i?_!hdJBn(S+V8dgLc%?FRdc=%& zFGe>^p(r53Z`Qo1=O*((tG7jI!$qEJrP8> z!e{y|)L<4mF{B|6#1cOQL54Q41I4sYOn2mV&1mMnqqH4PFh!D#U1h8j^{Vn+l=Ae_ zvaRnzS(<(5a{i^;?pRzD+89PwwmdDmFAoXVrh~{5*F}&O>00Hoo1C8SqDn=sWiK|C ze|nUksMk1H>wMUZ?HF5_Ny=L6%yI53G}p*29coKt)LfI-dJtY~)zf3DB zeHp3%05+rdz{nB#RPn;dBk%&dMKH=Mi&JSghL@XRlZb_GfoI8yng+?q6 z%FHM%9}ic9aWklwLMu9HyFT*6*7li$$8jWAk;YzRtX#(l{?{IAUG6gahyFi&Q7O9i zhsOqmld*7n;%YFQ!0!pnLaZ6VuHA_gP|icGT9{0lTWT=0ZV=oDty$vQFAi%wg4s@` z1^F5mbU~=#W_T;CB2%=GS)GWtM~S@#+dW^QGmx=r?Su0$w`s4FH}vf#k|l6kxPLBA zaV{#tslqMspfg9vwn0lE^$L)7`v=3woXdR})u0rK|KLw7-2My%?;Q|4DaR)1!>@>K zS@Y1^L52QUNY82`rHm{#kV8>1OYzGrdEr^5ttS1mwnh^%0`X2RjPfU8M<`%$&{M_Dwf1`T8uMB`=>RDBnD+erW?a=Nx=Cw=ECf)GY&mNYtFlM_UvSgeQgajd zp`-UFu5hKs_HSXMQ`7ZIZA^=+t?K!TG@xss`EQAC`d|J=d3d*=-4=qZ<{0@JwfKiu zm{OX@3)!s{PkL?5TKnQF8Eva0@t!<2H;g(q{^pQol*MuhVc{2J=PPyFld|22txu+j ziA#6a>*kWJ*s?^|_K&Hyf9&=5EIb^=WJ@f*;17PEF*abKUr%MSS9Ph@Dz(MXg$o-$ zypB8@DY?J@i5C+FvJyKTCrVQaIr{Cxw&WN$ck_(7zYcA;EN9Icq?P)1E1cM;6+ZfP zDfl=RaXVJEHhPcL&122M=1DYKJB`i`D^KrpyTA=D-)%(Sr26%86Fl<&m}BP{3C7lo1Ql=Dmu(ejrafL&n|)^~JEBa8jMab6K3e zn`ZHBAM?q@v6+`;sjq)=$#lY@wVx*<>S6^LbagLgNiKhr1otr9= zQi7yfmGi`BjT&P5%@ual4?~%=YwWRm_*|Z^jv8fr*Hr=QE^e$8O|epm$ ztXs}`ZD!f;k}IZ{vJd-`#FdVBDQ3T}E)iiDk@7^XgDLy?#3gBt>V>YnSYdY@2rI}S zqTio7G5nyNL36hb3^yO{3aXIIV5K0vG1?ewN>@ADBw5OoV!hg^L;2*^y_3u&74p~b z=;({qi=Kx(WGXI$*sH|E$}6;^%SuSYT{1eCS?sQlO-+{LH9?slc3oo!VqMQmDNA%F z$xE8K)EurXo4=N++rxb0F{O`#~_bFDb#aVjpeG*rz z6DjnLM@h>{a@I#1#Y-|jd^T+6#x8;8wD@pmH|_q0dSZaIdMUKfWI;B2S`0xyqO)7lsE5&x66t=E>Kj@dE3m}gz3HG@12>#xHM~`tr!E-CT!TdD`{rCeS>w%#mc%qM9oC^21 z1YN`X0INKXP??YK?&tSWVG31CzB~~By9$(&11VnkZp@==1}n{+@%WJ(Va#TSma|yE zadqlfm6WYlX~>8p&U_ZHM$#kJe>T?l=RxmEy)_LmOUgMU4HVYtdqkyIpV-y;30PT3bZ1B;@+Om2r7^_ z_C4{aI%RomB-?gpPQux07EjJD$-x}W>9F_UtV1YoMB}VmJJS$fa*y=UWdFybQ?+ zv=XzfYWCzkG=qx69Cc7TOO~S&jwmVWeEVaHe0nH-eenCquU^eSk1Kp2647WB>0*jQ zkC;K{ff59cv>mAyRt@%-h^dD?^&f%i>YW}yNBg?XYcDktVKj-vB}Cjt{ZD_oizNYs z9qftG#&*}Mqy}TGR6gf;aYarRGO6T#2i`Z!HMPWt;tb+SUEk%lnJbe8Lr2i&lrQar zAV;rQ=p-iV*woDJ-JEmo3qh1!Pwt6SRCTM{@uN5ibdU5M$J7O;*^_y_OS_%NChne<)3Cx>D*T8sff)4)f$b*!;xD<0XBmkinkSIrep5q#=QI>*WEMMiG=5%=%2%;;j(iLK`<@2htQ>7D z=o|oEx1w0wrh*D!25eQO`MA^ zcxOSd;-3?%fj=$juTkcLXPv!gL==PKGh6M!l}*;+t9fwg)GwH+R))uWf9oc%eGJs1 zB_D`H`Y^%pv!g0a4a1S7Gjq0g>NU}tZsKRNpFzmlss>N+;^qziW>(qW8B`&e*^nx2UECYJXkck$RSnj~DUhDVu}2Wj}C436LM>g;2$* zh~V0(xc#s*xPwqVvg#;wZKsO{2}W~8<`c5a81s)!Y>lq;)w-HM;iiM9eGwGCwRQWl zmt^xrkG(;Ys^Q~M#>$04n+@{JnEhN4jX}j1F5SpFl6dRcP%D*IM$UZ-@{VzxD7quN zENGWVa=dMSyIwqwcaDWBQ9oNok8^l17XxT(tN*g;h3D!6;vDY|yr6-}g)~3*O@^?K zPEY|`pirIy8Ci|di16YS@TA*(o`vnD%gwY+krqr@M(_jCZ0*)ep1uZ0O+BR*yCCsx z!LcP3P=~*|>=#F0f~P;d!p-9i9X;{3n|0kc^WLRFq_kn$Dre;{QRF~4+JHQ1_!hHD zp9T+yMN8~K@4BZ7VlA>p0LtVf%4uP7%Xvf79@m_(HH{^&Ze!v0Jz*i@diw)=aS#)_ zf<_twZEiA(>bZjZ!XfxBfI z`!*KA(e?*he9qbEIUd*@M;xF++oX6i{)>cx@faF-y5^5ctiL@N?ZmN;P4a*tL4!xTo zV|0_hc@R;os*U>?0e@<1Y&B}mhk2XPF;1CMhnV!TBoXH7aCQjHrn#Oz{l-u(|F9u* zNC*w<(GQXkT)+6{MAr}$ucAHuDem6wq%`As7x@f&w#89pb{7L=I8E;8!NLk#;wh~y z*DEsg_o{LP_IfNjc_ezyvU-%HN8hJ-nbYjUtH0l4>L)Q9IY>31)EaWqg~nSRf;w(r zw%G05SW&=GH=cje7VIL!sVR)f6#u=Uu0n8(j}Ag2-p4X;ShEmyj6}np0rB?K@`{U) zVj)mq&ZQi|N@R|o4PJ&%qdc0c?Fp(zFcXQzLzM$ zCC89yX14E3ZS!*kRzOuE*S-+OkQ$U2c7_$AV~4PaNJ+QaLnxcXjxv8bBnobDg&VD^ zl2ftq!+hkJZF-$7Id7d1f8dAT%?s|rf-EcwS5q9Ud+VHAyf2%eK5&<(M1h&6B=UM& ze#-@f(2BTbI?6r?erS0X0y-P5M)k0uTlIuy8B2waPQcCSzwR&D zH|MX%PpB??#xM?=)6^N!_mmf?=o#2oEod;36hNyUInOOS%!J35T*EIwjDF}Cb0xPzSBH4&x+Z!}Z|8NX+i@73U9%zH*mh;nt1)MODN9!YPRjQ`rACL>#UI zml>jKIq0|@3(T^=nc_T}Bx5xmeJC}rIkMqRE@#D6{x6^k*DVUo`^~Q#ROOXl5aupo}Yr=~>P0lgOkC$~>5Z6E4;3FfM}k2pscUA14ZP%j4E?75f(J=4)# zTbm0uL0@`QvTBF1Azrx}+VgO1Jt}n}%Yn&mjFJUtK}@0@G;mT-UMb_bGOc|@<_jiP z(SzQfAUvqZUm>LD{q6Xw1#@J0d;a3Eo`^4CiZ^*H3A6Yw+5gVKUivG+;)lEN3?88n-o0#G_l#h!puTnc8c+)YP zzFH`6-E7Nx2Xv5(zYx~NqmU0#n!sDioMP-1?GsV<%8=pJ3@w)@)1XYhyw=dkShe3) z)w1ES67~FTZ^+cyOz>QU4HM$2c`_BAULI+oIV_{4am1!Rzf}&7(`)LD*q$Gep~aeN zXw3M&wP|+cV6BpFJV8;%krzvXL_LO2NKt$Qy~^3i4wwz=ix~ae6z^QIiGSD+R}2Tj zLu}IgE2~Lua_YdG@KYx|J+^;s*{i%iOj3=MJI&mO8eyx~gLB?~Sshs!YA|}E9b9yO z-;4dB2)xCgLRgx#YAyWY@Q|ab{T^!kk*H4Mj?upH3_}pL)r**{^(4*~2yh`qAYx!s zqLS!LVXHGy~l7(vintiJN;~2Hzc3DTNeTeluT5Ivc&{YAquvDIWj#R~Z0-!4lB6^KA9IV>vW#TrFh z+Ed)YWmfFrs}U;GaTymh#ao=!aqkAfcAhRBF&#!NS%+9&E=^?kV{%g~M12*A@{~fJ za>-TL7P*%};&fqfozG}!-;}VGFG%x4;EfnLaI}Z&NvlABqx6_ep2{S~OWyWgy3pgE zEeZx182~pKK+;LW5hyj=fAQIFHg8qJ3GEk~5eZrar{RXguJMq>n!30Mdm{Ys60qBM z@%1!xsGFe08ebiJJ!8jJ_7bcC#W!?h=<+4+=KNQ(Q-6+{wuxjd)}f3>QT461{iKL6 z4paZidJf-Dgmh^jqmOyJM1N~Ry3Pg1;f3Yqyma^$WHg&bd$clfVD{cT0;Tl=%5Zo9 zo@e!VH9_kdImZXAtEYdiR?TS|hREb3c~RB0_Kv;JXjP$)mBirv*j(kQ z8LIXPU+K8COqp9pHt)ktWjY9co1#!VqtakEWoxAa!pK0%P~@7gk*@v2tHJ0b(lMzA zUkkOwwO|w8W&!)@K__~r4!JE1m*NrQ(4MKuY+KANF3Ql?Y%_^{>a zN8cvIGjqNyd?!SYWve#6JX-weBJxvjlL@RL;fpkM5(9x`GL@rv+QPu8_*gg??!$A4 z2QMIb=xTvmfZQq=Sx`hT}v3xTS z=R`fdjjyZP48oljTm9RRaN1_@qesZ|+~}vrKesX7tRF#5T#*S7SpT}~FMp;>%w8O1 zmb#)2GnFhIb#9RB1vOCS#i8&Z&fX8)!PcIi%ab%#iX;Wc`N4T@`dub{1NvZ4gp->O zB}2e^)YA=iBz})aMJT%ioZMG%=VSa!7>I5;A8^{ZUW|${3^`B39+L=ir#lNL5>smu zL~C2ouIkIi!H8#3LQWW?l_sk=6h6RCAuU~;HMizdS{2G0%cmCNKFl+GZUdp84U&!hWEIj12aKxg{)<-6OqzgxD}MNh zt{H)BBblWk{*eRmMxXiH{a3v8$CJBWKl|p;ScBQb|Jgizc(Cb|#!D*y`pH&^pRP#Z z^qA|l23cz0gMHKK<{iZ#{6XfLKjxnc(me`YsTUoBTy-AN{kpRYWZ=JLS%Ry>-UkP- zoRG2O(AVl?2cr-2O~Oc2BA3=h*y1SbVE)7zO6jToT-fD3+ZsbR&b1uP5an4jpT6Kr z>}5KTA4pYWA}}2{0Wa5l!a*3h3i}5hS?w`Fk?5j6w`Ni*#^*Qs5@f2A%T2Y_--%D7 z8{P}~MzDZwXT$#Bi0De`Ad4uYhrC2>+VrUuQ71K0i>~AY8)BmT(5ah=e5cPxmzGy8 zp(I*KF11INB~pF*nfjCV5I=@beroj+Z==U^CRta6`#_eUw8lWm{&pj*pJg(tbm8_< z*>P0w8}q5480NqHTvrYqMp@1~u=efGJuzbN;giUB>CM10T^bGm97j#dk_>hk%C^_n z#dhCPhH*@G{NqnU#uaQx=-q~*sS+@VuXY;`ZFNh{B_?ty+zLPbTjT>O zF-uDK8uhNH=<%b@Eask{wHG|@`L_Q%Lf!0&(cE%(x5fLK>A3yjRbK5pei)mNHh;`s zh;gBE7pKL}M9z9O^=_-vEMu4Pnt5fTrD3Ip&SE=L$9h!;;Nm!E8#W4f{Lrb=!f7_M zRkdElYPM4pv(_F1wDHf(05L89qdfnQ^80@8+#I?FmZ?wsrG~|a0O)Dd^Kdzzn98-e z`)9JUvw3Bk#p!vrYZdJ~M?_a}e^z2Yo=li?W@^5`2{L+i{$}Uym_LjD$r)94;#?^W? zWV+@5z~sF|j0vSkC>3(YN4MaoX@UN#XKFzDA(PJjmGaJg6`~@B0FOP{_TvxfXKwX0 z?@iBUUp}uVltN0==ejyhW#`bY6}gSGD|=b#uukjGqIdbI*Ca3g$w};mOq_9-E}GxU zXQ$Msi^VpP3I;b2E3J5pE%T}?TX!@Il%_+M0yi3n3z@hYOhv22zluP)ZwHi@sbUm(Dv;Akha4KpgNj#ZbD$A357+6pZj;%HCcr;sUk#aBXS^ukx2fP{e zm{YYp)8(1&YS!I;@yn%@3g0MOHfyyjNXfN0(_QOfINDK}+0&7coBm6mYi~6ZUf$Q8 zLvPc1n23aN0ZV>vU{cR&wY?xjauaTAwg1))!)>mW83QJDwbhu8=gzyd`!(W?9Nh=F zIUoXc3amoX@LYq%5Xj<9SnpgyxERdM)zgZ=P7!2x)dTssoMX{bkQC05E!@bLa;|?D z=A)~+Edg@xJ?)bJ|^P=KEW3+>3SxVZ4WTg?Iexhng^Qf4e!R>l4 zD-x$`$n(pcXvX(`OnrQ}@%$Hv9wTd-#Tf30p7Q4Pi*>a-W!a_t#doMTnvKV=Xe8ao za>69i!Xl}gU2NZ69P*ncfX3P6X#c`MC!lxP}#nN$~ z8md6Pia-Gf9#oihXZ1_TjR<3I_gXf;jAEii<7sfzu@Cb0Gfo{Q)U8F}ceQu__Gq8V6Kc;^rN(c$Do?!PCVCo>*)sBe*u9 z6;O_VzPvdjQd27lKb3J3zV7_y-3%gqzl(jspv zKvIlM)}9s0I~_Xf*9TXmKvJrkh3;PZI6RSpAG*xMbiXq zFb*?Dws4JI5RRPO7_}j>t2+Jy($%y zX*lFBQxeL1b;Zsqe7q&@N~AxbtGMbaVXacys`jsu$hVQcu9*K~n|>6brQ$6CkUO{H zY^?BQbTJDR5L@8-HG>)+=-oEtV1z?9Vzj zhO|pBcoiBOf#^3Ygl?sh{XT@P%(HpeP2KlCSJruux{1>lyjP&8ZPmqTa_zH(@H1|I ziwlCfT)oq0yIaMVCCIi9IPnC69PfPqD(&bnjC z1XaSFDd9i9Nf2^)Mymf zMIMntK^w3ues-w$RHHPqcRo9jv5uv^1(4u#yrI2hfyFq4#FaLL_9C3keC6*m$O7d3 zc^oSX?EhA7M5i0R<7l_rZLyVo;OOejrlBa_d+X`LIl<&3~zwXuzh<1NFX`MCbDEv=; zR94kGa4DT*`Kud;qZ9>pQTIAoNl-#nM#jo)7sd}f!B;7N>$k1wl$fAwDL*lfwSU><}Oh$pYZIxwJOC^Zd< zNEnzhltDI0997oQ*^n>ROKkBfEaIm94zj9wSu(XDOmAF*G4j+T#&DH?oY9lfUJ3?{ zyfawe=u2<*QjeYEM1~XSEmPU9qA!kz(TFks_FkI7!{p1nn!5?WwT?>V@+ScV9qM)o z2W4P0s_@6PU5K!$O@>IQta$djp2I{5Z(W!$Yu)-!p#Vof1)h4b&8CwC75qgwtjEWK z8%T96@;s;CRkT)?8cQ;4QvP^c&P3nJoC z48!NYAyOt&-dwM!*jfof#*}r8?A^LG7s+hs!#JK zc8Xz}(g3uKXlsMIdJrprTI{K}!Ml|J<-Hg4>(){T4;mn6B zSmo2oE>Jh?-j{u0_~ROg0`SFNn$R-1nTu7kpG^xmK0+9nMq4;%dnae8tv}q8agiB$ zfi=rEIOgSC;X>g@y$DMAl`OtB&iZQ{?2h8UvSX04jB|_5UxH(PB5RaZ{#Cu4qZ;yE zU+MF?O~zAfv!|%}%PRMU2ci!?VK~`1T<-yp!Igp)iTJdP0vchTMDcaj<*EHs@Jk8Z z6U-38+?F$0llre3mXzx@T!DT5f>k`AfIfvv4kXb*qS-aN97J6rc)QU3_w~9}CjT+m z6b_^eL{tI_RF<9DYXkvQE1X`ebCMtsr?6e5Y6C^ziRxjxKb276%7b7%CQbv6<2c?H zEBPl`FhxQR$RT^c8zgODZ$W@x*)vll-V0^v>eF;=m;BzxWGFT zd-gt^Lqh&DYGY(L77>5_wGbbJWJD7Cy(W1I?(HlcyMv`}mUFSj{ zJVcfaMwrtwUV2!_@4v=)!Tpm*wpUPx^X~-0tqSo7KmV;Y%Lwrsmh9rXuMvcjfHy zU6IE_rdiq)U7zZ|KPI|XG5)G!-~McbdYg7xin{AeoQkA40YdfbBmk;SpUGWKwcalP zW1w}aF{;Owgu#z!WjKV6ew)+>8^nDp-<#6%yI?1}r)i(G0WXX3qd(9;({~V`0=B(e z)#0`u9zHoSN;MhQmVUxVZm{TFrM6Yh%G){+q!Sqpm&8;VS2;=kZ)= z?>eJ%ag3@zwpsR*s(`&n`uI5dEllNiAVaZUu+YcO7#!tHU_($9R{=~v9BKVe126-;>Q z6-Z;M1v=r=fjVDtAoS$cFlLw;&2@l&$LUqNTC;V611>;4yfS#BEU5O|1`x!MD%??r)m8~#sI?T)B);8Mi+*C4b%FjX|{>wmF&;KQx9kgj%^|CyqaN6;%R0wDF;PjV#;b zLy~M|##o_BWTs=VAamgo8x<`*XRfLO_5x@HpRs0)JemJIRfe#$_!4`7@O#29vG-$0 zGWR&SjgxZPgneH4j<5yDYF&*=)8!HK^;uu6^+C{S{uh>5AzpFdLa|@npKKs)>$C9W z2W(ll4W>NNep6tGo4-(mjoFkjSq9Z3T|LVZjJn|O$o~`C3MKVj(M-Ae6-7y1$I5*M z!5(Ge8Dx252hxgU80W7*V{EO~=pN&mp1D~2fp;gKoKF;|pWgFV-ac&xJq(qG=zfP0 zULLXvn=TrKNb6C&dO4QZv2}*O=UA1lT4v>Er1)DxR`HU$zp6#YT3uaRevcHH@UKUV zeALxd!>+^aS@(Q=?I$hOQYuuKy=ywtMh~Vibr>rZ2vJ02AxqD4VdW7=dGtpHA0ve@wNSsr@t^p;X$G_tcRr)lx);bEpH!Z7ghNSnMqh(X$0?MhxW&HgoEJ zwW+@7-(7<#!GQ#eqD&~|BuOAmMyTx-3NZ&s0li{mswRx1wkfssSrrmk5LscqJ zslZ<@p?d1`iE^)kLN=eL7>Nzh%~V#hg2i_DzVEgB3aW)cM#CfMEzCesjGGZuA=OoKen_?|(r~(I(CB+{MEE~t~37@}dQ`YZ&v$A{icEhq| z*fGcKhmu;l6jn`xryPhPsA?z_t$Qk>W@dXOIM?9)G&4{=87x@TeE>v7nuFZI_qc^Y z{=*STv@oO9(>PNufZHbivQW+Ee6|RE3JlXM@@evJq% z6)Y)J@eX3E5iHK45UK%1LfAmCgDG!b%jWOAjzix3BP?V-l~y|@i^>MJZb_JXU<~3v zG2SWkHA$Ze7S=j2GP;BA($p*o-XKvL1+zIO78$lgaI9s1l=)=a9`%6oQY<#k+G+%` z0$U`#D;XNNAnu!%TypWHob&W5YdM7?15ZrhLlHLJ2z4-&H*pYV!lE3uab+O9r=zla zUAfftY{%XR)@O~HHI0C~n*YnZzt*^qZFKw`LpP;#lFdN3fSrDS%fvqnsWu7^>qOUO z!8`X1gMjVEY*In}yU=|;ZEy4eiWZnn4~TOxK0#)E{0;QQ)YG3tA-)5$-BQc78lA-$ z%yRR|*jc^VrrY7&7IjR+Yz}B1W(Oe6Hkc~2nFhU}5?BWiBaI3^zrJ$arSm%;sOUov zH3J{mMR>WRml~mHZL#b&++*%A zx4K0Z1pOj@ZwDJM8}TI&${7o6&iyP7lQc>HcyymDsjKyF9GM>5^DA(xis8FB-iwYAqAb}|MG=kJ1jxhx}Zc#KWPx#Z> zZ{p;yehz>hRz18z_>c&PgseJ2u9q?tc!r0yf}kJL&x(<6bkkF+lD^vEIhG|VHvmVi zFf5uL7RxQM;4m!gqw=8(&AKJ)1X8dA?(43Yq)Gaxp!-}&Ez>ddmti+@7Ti(fP~ z#P$t$o(hc}zs<7pEn5VC23=|5FAl3mQ~JB(EOR|oju&%5I?Pa9z=i@*u}G<81f^sK zT2{$4b8sd+FWSuSQs<5+rib)V73>3og@)B~f0OFICje#cfd6iW3_ zf_)9=XsnUc&<-5Pw&_`g_?zHHOt-nT(O|c^hAoO3kX28d5E&QUYMgmY^EPK~Sj7eN z52KTH%uh$d`l$0iMEYMM%rPv<3wtR84r4;Gu$u78p!P{IZB2>2OHy>M(1RdKEk}*h z@RGMz=QlzQ%(2K}CxlN~b?B!mcBcRly(Bi}e(8cqnxuagx?hyk)~#ZCM?!IGhSigF zl9_!3c*MAO8WX`&R>pN0W0%1lpl!fL!3GyMn(OKi7`^D$kSQrGgZ+~VOvI zb)E7$<5|SbQ7Y^fLtl?bF0av1>85gA1zscU!8CnUIp3N~yQX72`zv2AZVyBkMI;xB zcpzpJ$4Oa0Vx4!DRg*MH|F-lGfgIG%tuS*gNm?OTfnY|he_IsyP4C6xE-Z8aCVAE5 zqu@gpie<>n8P9SWqfq1tRJoBgjM4!Nh8jH&6#n9b@VG$ej_UPQ2ok~4tgzTxNWTxd z*eLDnBIjWgO3 z&?ae;Ch70fKh%ivjlvFs%8E_I^LhwfJ$V`QdU~Gc5X%H5gIQ+WmKyU9*-W49<__jC z#1sxhmvBSI@6`30oEEHQiI zD0}Ilyr4?_EXST=8781cbF=MMR+%!tSOlI5b_m^Mgv&vH6=5VRNLeIh2U&9ZBu&yJ z-Ceq09Ork30!IK5#%k*&wF-r(LevdDuY`{3W7Pv49t_bUgRZ=m6Pd3_Wx*yRy%zMV z05^$vAjRmWe|7<$RHg7_2n(~yt|WCdXT8K{p0*kCN}0AEz$GfGVziscDTa=&l_KKarB!OoapKVri+U($uQi%mijZtvezr zd4@eNLl{YjcA3~&u>L(|rdJ^)BBetK#o=x`hx=^Lter;^(g#DBBccw^AOk=MPDsE9 z1UqANhrwnk*;25zgQFZF5Cu|>rouDm=VuhS)*gI+vuKhg>0grmKP&GlBIC(8k^lez M07*qoM6N<$f_T)fHUIzs literal 0 HcmV?d00001 diff --git a/website/public/assets/logo-light.png b/website/public/assets/logo-light.png new file mode 100644 index 0000000000000000000000000000000000000000..d488ef11091b77fc2749f5fd53716ba88cc92ea7 GIT binary patch literal 17032 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 literal 0 HcmV?d00001 diff --git a/website/public/assets/og-sqlclean-docs.png b/website/public/assets/og-sqlclean-docs.png new file mode 100644 index 0000000000000000000000000000000000000000..61a85f39dc26490f87eaa35d20a99b23dbe52041 GIT binary patch literal 283773 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