diff --git a/.github/workflows/sqlguard-ci.yml b/.github/workflows/sqlguard-ci.yml index b7e3beb..31a8d46 100644 --- a/.github/workflows/sqlguard-ci.yml +++ b/.github/workflows/sqlguard-ci.yml @@ -4,6 +4,8 @@ on: push: branches: - main + tags: + - "*" pull_request: branches: - main @@ -37,7 +39,7 @@ jobs: - name: Run Tox suite run: tox run - publish-sqlguard: + build-sqlguard: needs: [test-sqlguard, test-tox-sqlguard] if: startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest @@ -46,10 +48,28 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 - - - name: Build and publish package - env: - PYPI_API_TOKEN: ${{ secrets.PYPI_API_TOKEN }} - run: | - uv build - uv publish + - name: Build package + run: uv build + - name: "Upload Artifact" + uses: actions/upload-artifact@v4 + with: + name: sqlguard-dist + path: dist + retention-days: 5 + publish-sqlguard: + needs: [build-sqlguard] + if: startsWith(github.ref, 'refs/tags/') + runs-on: ubuntu-latest + permissions: + # IMPORTANT: this permission is mandatory for Trusted Publishing + id-token: write + # Needed for fetching the code + contents: read + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + name: sqlguard-dist + path: dist + - name: Publish package distributions to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 diff --git a/README.md b/README.md new file mode 100644 index 0000000..971b7d1 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +# pytest-sqlguard 🔍🛡️ + +[![pytest](https://img.shields.io/badge/pytest-extension-blue)](https://docs.pytest.org/en/latest/) +[![Python](https://img.shields.io/pypi/v/pytest-sqlguard)](https://pypi.org/project/pytest-sqlguard/) + + +--- + +`pytest-sqlguard` is a pytest plugin that helps you safeguard your SQLAlchemy queries against unintended changes. +It records all database queries executed during tests and compares them against previously recorded "reference" queries, +alerting you to any differences or regressions that may impact SQL functionality or performance. + +✅ Easily detect unintended database query changes, regressions + +✅ Prevent accidental database performance degradation + +✅ Simple integration with SQLAlchemy & pytest + + +## Installation 📦 + +```bash +uv add pytest-sqlguard +``` + +## Usage 🚀 + +Configure your fixture as you like and then it can be as simple as: + +```python +def test_your_database_logic(sqlguard, session): + with sqlguard(session): + test_client.get("/users/random_id") +``` + +When you run the test for the first time, pytest-sqlguard records all queries executed during the test and stores them as "reference data". + +On subsequent test runs, it compares current queries against the reference, alerting you if any differences are found. +The stored data is in a _yaml_ file named as your test file but with the `.queries.yaml` extension: + +```yaml +test_your_database_logic: + queries: + - statement: |- + SELECT ... + FROM public.users + WHERE public.users.id = %(pk_1)s::UUID +``` + +## Command Line Options ⚙️ + +The extension provides two configurable behaviors (that defaults to False for both of them) +1. **Fail Missing**: Fail if the queries are missing in the reference file. +2. **Overwrite**: If set to True, it will overwrite the reference query + +You can add option flag to pytest to control those behaviors: + +```python +# In your conftest.py +def pytest_addoption(parser): + sqlguard_group = parser.getgroup("sqlguard", "Options for SQLGuard") + sqlguard_group.addoption( + "--sqlguard-fail-missing", + dest="sqlguard_fail_missing", + default=False, + action="store_true", + help="SQLGuard: Fail if queries are missing in the stored reference file.", + ) + sqlguard_group.addoption( + "--sqlguard-overwrite", + dest="sqlguard_overwrite", + default=False, + action="store_true", + help="SQLGuard: Overwrite the stored reference file.", + ) +``` + +For example, to trigger an error if the stored reference queries are missing, run: + +```bash +pytest --sqlguard-fail-missing +``` + +To update reference queries after intended SQL changes, use: + +```bash +pytest --sqlguard-overwrite +``` + +## Configuring the Fixture in Tests ⚗️ +You need to define your fixture in your `conftest.py` file. +Here is an example of a working fixture: +```python +@pytest.fixture(scope="function") +def sqlguard(request, tmp_path_factory, session): + # We use the previously defined pytest cli arguments + fail_on_missing_reference_data = request.config.option.sqlguard_fail_missing + overwrite_reference_data = request.config.option.sqlguard_overwrite + + from pytest_sqlguard.sqlguard import sqlguard as sqlguard_function + + yield sqlguard_function( + request, + tmp_path_factory, + fail_on_missing_reference_data, + overwrite_reference_data, + ) +``` + +You can also use the assert representation we created to show the differences: + +```python +# In conftest.py +from pytest_sqlguard.sqlguard import assertrepr_compare + +def pytest_assertrepr_compare(config, op, left, right): + return assertrepr_compare(config, op, left, right) +``` + +## Contributing 🤝 + +Issues, feature requests, and contributions are warmly welcomed! Feel free to open a pull request or submit an issue. + +## License 📜 + +`pytest-sqlguard` is distributed under the MIT license. See [LICENSE](./LICENSE.md) for details. diff --git a/pyproject.toml b/pyproject.toml index a8b1ee6..0a41948 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,8 +1,10 @@ [project] name = "pytest-sqlguard" -version = "2025.305.2" +version = "2025.305.10" description = "Pytest fixture to record and check SQL Queries made by SQLAlchemy" authors = [{ name = "Manu", email = "manuel.vives@paylead.fr" }] +readme = "README.md" +license = "MIT" requires-python = ">=3.8" dependencies = [ "filelock>=3", @@ -12,6 +14,7 @@ dependencies = [ "sqlparse>=0.4", ] classifiers = [ + "License :: OSI Approved :: MIT License", "Development Status :: 4 - Beta", # Indicate who your project is intended for @@ -88,7 +91,7 @@ pythonPlatform = "Linux" executionEnvironments = [{ root = "src" }] [tool.bumpver] -current_version = "2025.305.2" +current_version = "2025.305.10" version_pattern = "YYYY.MM0D.INC0[-TAG]" commit_message = "bump version {old_version} -> {new_version}" tag_message = "{new_version}"