diff -pruN 1.3.3-2/.github/workflows/pypi.yml 2.0.0-0ubuntu1/.github/workflows/pypi.yml
--- 1.3.3-2/.github/workflows/pypi.yml	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/.github/workflows/pypi.yml	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,68 @@
+name: Publish Python distribution to PyPI and TestPyPI
+# Source:
+# https://packaging.python.org/en/latest/guides/publishing-package-distribution-releases-using-github-actions-ci-cd-workflows/
+on:
+  push:
+    branches:
+      - main
+    tags:
+      - 'v*'
+  workflow_dispatch:
+jobs:
+  build:
+    name: Build distribution package
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v4
+      - uses: astral-sh/setup-uv@v6
+        with:
+          version: '>=0.7'
+      - name: Check for version match in git tag and django_cte.__version__
+        if: startsWith(github.ref, 'refs/tags/v')
+        run: uvx pyverno check django_cte/__init__.py "${{ github.ref }}"
+      - name: Add untagged version suffix
+        if: ${{ ! startsWith(github.ref, 'refs/tags/v') }}
+        run: uvx pyverno update django_cte/__init__.py
+      - name: Build a binary wheel and a source tarball
+        run: uv build
+      - name: Store the distribution packages
+        uses: actions/upload-artifact@v4
+        with:
+          name: python-package-distributions
+          path: dist/
+  pypi-publish:
+    name: Upload release to PyPI
+    needs: [build]
+    runs-on: ubuntu-latest
+    environment:
+      name: pypi
+      url: https://pypi.org/p/django-cte
+    permissions:
+      id-token: write
+    steps:
+      - name: Download all the dists
+        uses: actions/download-artifact@v4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish package distributions to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+  pypi-test-publish:
+    name: Upload release to test PyPI
+    needs: [build]
+    runs-on: ubuntu-latest
+    environment:
+      name: testpypi
+      url: https://test.pypi.org/p/django-cte
+    permissions:
+      id-token: write
+    steps:
+      - name: Download all the dists
+        uses: actions/download-artifact@v4
+        with:
+          name: python-package-distributions
+          path: dist/
+      - name: Publish package distributions to PyPI
+        uses: pypa/gh-action-pypi-publish@release/v1
+        with:
+          repository-url: https://test.pypi.org/legacy/
diff -pruN 1.3.3-2/.github/workflows/tests.yml 2.0.0-0ubuntu1/.github/workflows/tests.yml
--- 1.3.3-2/.github/workflows/tests.yml	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/.github/workflows/tests.yml	2025-06-16 15:26:31.000000000 +0000
@@ -1,21 +1,54 @@
 name: django-cte tests
 on:
   push:
-    branches: [master]
+    branches: [main]
   pull_request:
-    branches: [master]
+    branches: [main]
+  workflow_dispatch:
 
 jobs:
+  configure:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v4
+    - name: Read Python versions from pyproject.toml
+      id: read-python-versions
+      # produces output like: python_versions=[ "3.9", "3.10", "3.11", "3.12" ]
+      run: >-
+        echo "python_versions=$(
+        grep -oP '(?<=Language :: Python :: )\d\.\d+' pyproject.toml
+        | jq --raw-input .
+        | jq --slurp .
+        | tr '\n' ' '
+        )" >> $GITHUB_OUTPUT
+    - name: Read Django versions from pyproject.toml
+      id: read-django-versions
+      # django_versions=[ "Django~=4.2.0", "Django~=5.1.0", "Django~=5.2.0" ]
+      run: >-
+        echo "django_versions=$(
+        grep -oP '(?<=Framework :: Django :: )\d+\.\d+' pyproject.toml
+        | sed -E 's/(.+)/Django~=\1.0/'
+        | jq --raw-input .
+        | jq --slurp .
+        | tr '\n' ' '
+        )" >> $GITHUB_OUTPUT
+    outputs:
+      python_versions: ${{ steps.read-python-versions.outputs.python_versions }}
+      django_versions: ${{ steps.read-django-versions.outputs.django_versions }}
+
   tests:
+    needs: [configure]
     runs-on: ubuntu-latest
     strategy:
       fail-fast: false
       matrix:
-        python: ['3.8', '3.9', '3.10', '3.11', '3.12']
-        django:
-        - 'Django>=3.2,<3.3'
-        - 'Django>=4.1,<4.2'
-        - 'Django>=4.2,<4.3'
+        python: ${{ fromJSON(needs.configure.outputs.python_versions) }}
+        django: ${{ fromJSON(needs.configure.outputs.django_versions) }}
+        exclude:
+        - {python: '3.9', django: 'Django~=5.1.0'}
+        - {python: '3.9', django: 'Django~=5.2.0'}
+    env:
+      allowed_python_failure: '3.14'
     services:
       postgres:
         image: postgres:latest
@@ -32,15 +65,15 @@ jobs:
           --health-retries 5
     steps:
     - uses: actions/checkout@v3
-    - uses: actions/setup-python@v4
+    - uses: astral-sh/setup-uv@v6
       with:
+        version: '>=0.7'
         python-version: ${{ matrix.python }}
     - name: Setup
       run: |
-        python --version
-        pip install --upgrade pip wheel
-        pip install "${{ matrix.django }}" psycopg2-binary pynose flake8
-    - name: Run tests
+        uv sync --locked --no-install-package=django
+        uv pip install "${{ matrix.django }}"
+    - name: Run tests on PostgreSQL
       env:
         DB_SETTINGS: >-
           {
@@ -51,6 +84,10 @@ jobs:
             "HOST":"localhost",
             "PORT":"5432"
           }
-      run: nosetests -v --capture_output
+      run: .venv/bin/pytest -v
+      continue-on-error: ${{ matrix.python == env.allowed_python_failure }}
+    - name: Run tests on SQLite
+      run: .venv/bin/pytest -v
+      continue-on-error: ${{ matrix.python == env.allowed_python_failure }}
     - name: Check style
-      run: flake8 django_cte/ tests/
+      run: .venv/bin/ruff check
diff -pruN 1.3.3-2/CHANGELOG.md 2.0.0-0ubuntu1/CHANGELOG.md
--- 1.3.3-2/CHANGELOG.md	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/CHANGELOG.md	2025-06-16 15:26:31.000000000 +0000
@@ -1,5 +1,35 @@
 # Django CTE change log
 
+## 2.0.0 - 2025-06-16
+
+- **API overhaul**
+  - `With` has been renamed to `CTE`. `With` is deprecated and will be removed
+    in a future version of django-cte.
+  - `with_cte` was moved from a `CTEQuerySet` method to a stand-alone function.
+  - `CTEManager` and `CTEQuerySet` are deprecated and should be removed from
+    code that uses them, as they are no longer necessary. They will be removed
+    in a future version of django-cte.
+  - Reference the [documentation](https://dimagi.github.io/django-cte/) for new
+    usage patterns.
+- **BREAKING:** On Django 5.2 and later, the name specified in
+  `.values('fk_name')` must match the name of the same column referenced by
+  `cte.col.fk_name`—for example, in a join condition. It may end with `_id` or
+  not, but the references must be consistent. This change may require previously
+  working CTE queries to be adjusted when migrating to Django 5.2
+  ([example](https://github.com/dimagi/django-cte/commit/321d92cd8d1edd515c1f5000a3b12c35265aa4f8)).
+- Django 5.0 is EOL and no longer supported.
+- Fixed broken `UNION` and other "combined" queries.
+- Internally, the library has been updated to simplify the code and remove
+  workarounds for old and unsupported versions of Django.
+- Modernized development tooling
+  - Replaced _nosetests_ with _pytest_.
+  - Replaced _setup.py_ with _pyproject.toml_
+  - Replaced _flake8_ with _ruff_.
+  - Replaced _venv/pip_ with _uv_.
+  - Improved Github Actions automation, including automated releases.
+  - Dev versions of django-cte are now published on PyPI, making them easier to
+    test and use before an official release is cut.
+
 ## 1.3.3 - 2024-06-07
 
 - Handle empty result sets in CTEs ([#92](https://github.com/dimagi/django-cte/pull/92)).
diff -pruN 1.3.3-2/README.md 2.0.0-0ubuntu1/README.md
--- 1.3.3-2/README.md	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/README.md	2025-06-16 15:26:31.000000000 +0000
@@ -19,23 +19,34 @@ to use Common Table Expressions with the
 
 ```
 cd django-cte
-mkvirtualenv cte  # or however you choose to setup your environment
-pip install django pynose flake8
+uv sync
 
-nosetests
-flake8 --config=setup.cfg
+pytest
+ruff check
+
+# To run tests against postgres
+psql -U username -h localhost -p 5432 -c 'create database django_cte;'
+export PG_DB_SETTINGS='{
+    "ENGINE":"django.db.backends.postgresql_psycopg2",
+    "NAME":"django_cte",
+    "USER":"username",
+    "PASSWORD":"password",
+    "HOST":"localhost",
+    "PORT":"5432"}'
+
+# WARNING pytest will delete the test_django_cte database if it exists!
+DB_SETTINGS="$PG_DB_SETTINGS" pytest
 ```
 
 All feature and bug contributions are expected to be covered by tests.
 
 
-## Uploading to PyPI
+## Publishing a new verison to PyPI
 
-Package and upload the generated files.
+Push a new tag to Github using the format vX.Y.Z where X.Y.Z matches the version
+in [`__init__.py`](django_cte/__init__.py).
 
-```
-pip install -r pkg-requires.txt
+A new version is published to https://test.pypi.org/p/django-cte on every
+push to the *main* branch.
 
-python setup.py sdist bdist_wheel
-twine upload dist/*
-```
+Publishing is automated with [Github Actions](.github/workflows/pypi.yml).
diff -pruN 1.3.3-2/debian/.gitignore 2.0.0-0ubuntu1/debian/.gitignore
--- 1.3.3-2/debian/.gitignore	2024-12-12 16:52:27.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/.gitignore	1970-01-01 00:00:00.000000000 +0000
@@ -1 +0,0 @@
-/files
diff -pruN 1.3.3-2/debian/changelog 2.0.0-0ubuntu1/debian/changelog
--- 1.3.3-2/debian/changelog	2024-12-12 16:52:27.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/changelog	2025-08-13 18:14:02.000000000 +0000
@@ -1,3 +1,12 @@
+django-cte (2.0.0-0ubuntu1) questing; urgency=medium
+
+  * New upstream version 2.0.0
+  * d/p/pytest.patch: Remove - fixed upstream.
+  * d/p/remove-unmagic.patch: Use standard pytest fixtures instead of unmagic.
+  * d/control: Add flit build dependency.
+
+ -- Lena Voytek <lena.voytek@canonical.com>  Wed, 13 Aug 2025 14:14:02 -0400
+
 django-cte (1.3.3-2) unstable; urgency=medium
 
   * Team upload.
diff -pruN 1.3.3-2/debian/control 2.0.0-0ubuntu1/debian/control
--- 1.3.3-2/debian/control	2024-12-12 16:52:27.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/control	2025-08-13 18:14:02.000000000 +0000
@@ -1,5 +1,6 @@
 Source: django-cte
-Maintainer: Debian Python Team <team+python@tracker.debian.org>
+Maintainer: Ubuntu Developers <ubuntu-devel-discuss@lists.ubuntu.com>
+XSBC-Original-Maintainer: Debian Python Team <team+python@tracker.debian.org>
 Uploaders: Edward Betts <edward@4angle.com>
 Section: python
 Priority: optional
@@ -10,7 +11,8 @@ Build-Depends: debhelper-compat (= 13),
                python3-django,
                python3-pytest,
                python3-pytest-django,
-               python3-setuptools
+               python3-setuptools,
+               flit
 Rules-Requires-Root: no
 Standards-Version: 4.7.0
 Homepage: https://github.com/dimagi/django-cte
diff -pruN 1.3.3-2/debian/patches/pytest.patch 2.0.0-0ubuntu1/debian/patches/pytest.patch
--- 1.3.3-2/debian/patches/pytest.patch	2024-12-12 16:52:27.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/patches/pytest.patch	1970-01-01 00:00:00.000000000 +0000
@@ -1,241 +0,0 @@
-From: Jannis Vajen <jvajen@gmail.com>
-Date: Sat, 17 Sep 2022 07:11:07 +0200
-Subject: Use pytest instead of nose
-
-Origin: other, https://github.com/dimagi/django-cte/pull/63
-Bug-Debian: https://bugs.debian.org/1018341
-Last-Update: 2024-12-12
----
- README.md             |  6 ++--
- pyproject.toml        |  4 +++
- tests/__init__.py     | 21 -------------
- tests/conftest.py     | 64 +++++++++++++++++++++++++++++++++++++
- tests/django_setup.py | 87 ---------------------------------------------------
- 5 files changed, 71 insertions(+), 111 deletions(-)
- create mode 100644 pyproject.toml
- create mode 100644 tests/conftest.py
- delete mode 100644 tests/django_setup.py
-
-diff --git a/README.md b/README.md
-index d88121e..740cdd6 100644
---- a/README.md
-+++ b/README.md
-@@ -1,6 +1,6 @@
- # Common Table Expressions with Django
- 
--[![Build Status](https://github.com/dimagi/django-cte/actions/workflows/tests.yml/badge.svg)](https://github.com/dimagi/django-cte/actions/workflows/tests.yml)
-+[![Tests](https://github.com/dimagi/django-cte/actions/workflows/tests.yml/badge.svg)](https://github.com/dimagi/django-cte/actions/workflows/tests.yml)
- [![PyPI version](https://badge.fury.io/py/django-cte.svg)](https://badge.fury.io/py/django-cte)
- 
- ## Installation
-@@ -20,9 +20,9 @@ to use Common Table Expressions with the Django ORM.
- ```
- cd django-cte
- mkvirtualenv cte  # or however you choose to setup your environment
--pip install django pynose flake8
-+pip install django pytest-django flake8
- 
--nosetests
-+python -m pytest
- flake8 --config=setup.cfg
- ```
- 
-diff --git a/pyproject.toml b/pyproject.toml
-new file mode 100644
-index 0000000..8835ddb
---- /dev/null
-+++ b/pyproject.toml
-@@ -0,0 +1,3 @@
-+[tool.pytest.ini_options]
-+DJANGO_SETTINGS_MODULE = "tests.settings"
-+python_files = ["tests.py", "test_*.py", "*_tests.py"]
-diff --git a/tests/__init__.py b/tests/__init__.py
-index 4f610c4..e69de29 100644
---- a/tests/__init__.py
-+++ b/tests/__init__.py
-@@ -1,21 +1,1 @@
--from __future__ import absolute_import
--from __future__ import unicode_literals
--
--import os
--
--import django
--
--# django setup must occur before importing models
--os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
--django.setup()
--
--from .django_setup import init_db, destroy_db  # noqa
--
--
--def setup():
--    """Initialize database for nosetests"""
--    init_db()
--
--
--def teardown():
--    destroy_db()
-+# Almost-empty file to pacify dgit.
-diff --git a/tests/conftest.py b/tests/conftest.py
-new file mode 100644
-index 0000000..4f595ec
---- /dev/null
-+++ b/tests/conftest.py
-@@ -0,0 +1,64 @@
-+import pytest
-+
-+
-+@pytest.fixture(scope='session')
-+def django_db_setup(django_db_setup, django_db_blocker):
-+    with django_db_blocker.unblock():
-+        setup_data()
-+
-+
-+def setup_data():
-+    from .models import KeyPair, Region, Order
-+    regions = {None: None}
-+    for name, parent in [
-+        ("sun", None),
-+        ("mercury", "sun"),
-+        ("venus", "sun"),
-+        ("earth", "sun"),
-+        ("moon", "earth"),
-+        ("mars", "sun"),
-+        ("deimos", "mars"),
-+        ("phobos", "mars"),
-+        ("proxima centauri", None),
-+        ("proxima centauri b", "proxima centauri"),
-+        ("bernard's star", None),
-+    ]:
-+        region = Region(name=name, parent=regions[parent])
-+        region.save()
-+        regions[name] = region
-+
-+    for region, amount in [
-+        ("sun", 1000),
-+        ("mercury", 10),
-+        ("mercury", 11),
-+        ("mercury", 12),
-+        ("venus", 20),
-+        ("venus", 21),
-+        ("venus", 22),
-+        ("venus", 23),
-+        ("earth", 30),
-+        ("earth", 31),
-+        ("earth", 32),
-+        ("earth", 33),
-+        ("moon", 1),
-+        ("moon", 2),
-+        ("moon", 3),
-+        ("mars", 40),
-+        ("mars", 41),
-+        ("mars", 42),
-+        ("proxima centauri", 2000),
-+        ("proxima centauri b", 10),
-+        ("proxima centauri b", 11),
-+        ("proxima centauri b", 12),
-+    ]:
-+        order = Order(amount=amount, region=regions[region])
-+        order.save()
-+
-+    for key, value, parent in [
-+        ("level 1", 1, None),
-+        ("level 2", 1, "level 1"),
-+        ("level 2", 2, "level 1"),
-+        ("level 3", 1, "level 2"),
-+    ]:
-+        parent = parent and KeyPair.objects.filter(key=parent).first()
-+        KeyPair.objects.create(key=key, value=value, parent=parent)
-diff --git a/tests/django_setup.py b/tests/django_setup.py
-deleted file mode 100644
-index 806ddcd..0000000
---- a/tests/django_setup.py
-+++ /dev/null
-@@ -1,87 +0,0 @@
--from __future__ import absolute_import
--from __future__ import unicode_literals
--
--import sys
--
--from django.db import connection
--
--from .models import KeyPair, Region, Order
--
--is_initialized = False
--
--
--def init_db():
--    global is_initialized
--    if is_initialized:
--        return
--    is_initialized = True
--
--    # replace sys.stdout for prompt to delete database
--    old_stdout = sys.stdout
--    sys.stdout = sys.__stdout__
--    try:
--        connection.creation.create_test_db(verbosity=0)
--    finally:
--        sys.stdout = old_stdout
--
--    setup_data()
--
--
--def destroy_db():
--    connection.creation.destroy_test_db(verbosity=0)
--
--
--def setup_data():
--    regions = {None: None}
--    for name, parent in [
--        ("sun", None),
--        ("mercury", "sun"),
--        ("venus", "sun"),
--        ("earth", "sun"),
--        ("moon", "earth"),
--        ("mars", "sun"),
--        ("deimos", "mars"),
--        ("phobos", "mars"),
--        ("proxima centauri", None),
--        ("proxima centauri b", "proxima centauri"),
--        ("bernard's star", None),
--    ]:
--        region = Region(name=name, parent=regions[parent])
--        region.save()
--        regions[name] = region
--
--    for region, amount in [
--        ("sun", 1000),
--        ("mercury", 10),
--        ("mercury", 11),
--        ("mercury", 12),
--        ("venus", 20),
--        ("venus", 21),
--        ("venus", 22),
--        ("venus", 23),
--        ("earth", 30),
--        ("earth", 31),
--        ("earth", 32),
--        ("earth", 33),
--        ("moon", 1),
--        ("moon", 2),
--        ("moon", 3),
--        ("mars", 40),
--        ("mars", 41),
--        ("mars", 42),
--        ("proxima centauri", 2000),
--        ("proxima centauri b", 10),
--        ("proxima centauri b", 11),
--        ("proxima centauri b", 12),
--    ]:
--        order = Order(amount=amount, region=regions[region])
--        order.save()
--
--    for key, value, parent in [
--        ("level 1", 1, None),
--        ("level 2", 1, "level 1"),
--        ("level 2", 2, "level 1"),
--        ("level 3", 1, "level 2"),
--    ]:
--        parent = parent and KeyPair.objects.filter(key=parent).first()
--        KeyPair.objects.create(key=key, value=value, parent=parent)
diff -pruN 1.3.3-2/debian/patches/remove-unmagic.patch 2.0.0-0ubuntu1/debian/patches/remove-unmagic.patch
--- 1.3.3-2/debian/patches/remove-unmagic.patch	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/patches/remove-unmagic.patch	2025-08-13 18:13:52.000000000 +0000
@@ -0,0 +1,141 @@
+Description: Replace use of unmagic fixtures with standard pytest
+ pytest-unmagic is not available in the archive. Remove the need for the
+ dependency by using standard pytest fixtures.
+Author: Lena Voytek <lena.voytek@canonical.com>
+Last-Update: 2025-08-13
+---
+This patch header follows DEP-3: http://dep.debian.net/deps/dep3/
+--- a/pyproject.toml
++++ b/pyproject.toml
+@@ -36,7 +36,8 @@
+ [dependency-groups]
+ dev = [
+     "psycopg2-binary",
+-    "pytest-unmagic",
++    "pytest",
++    "pytest-django",
+     "ruff",
+ ]
+ 
+@@ -52,3 +53,7 @@
+ 
+ [tool.distutils.bdist_wheel]
+ universal = true
++
++[tool.pytest.ini_options]
++DJANGO_SETTINGS_MODULE = "tests.settings"
++addopts = "--reuse-db --nomigrations"
+--- a/tests/__init__.py
++++ b/tests/__init__.py
+@@ -1,24 +1,6 @@
+-import os
+ import warnings
+ from contextlib import contextmanager
+ 
+-import django
+-from unmagic import fixture
+-
+-# django setup must occur before importing models
+-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
+-django.setup()
+-
+-from .django_setup import init_db, destroy_db  # noqa
+-
+-
+-@fixture(autouse=__file__, scope="package")
+-def test_db():
+-    with ignore_v1_warnings():
+-        init_db()
+-    yield
+-    destroy_db()
+-
+ 
+ @contextmanager
+ def ignore_v1_warnings():
+--- /dev/null
++++ b/tests/conftest.py
+@@ -0,0 +1,45 @@
++import os
++import warnings
++from contextlib import contextmanager
++
++import django
++import pytest
++
++# django setup must occur before importing models
++os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
++django.setup()
++
++
++@pytest.fixture(scope="session", autouse=True)
++def django_db_setup(django_db_setup, django_db_blocker):
++    from .django_setup import init_db
++
++    with django_db_blocker.unblock():
++        with ignore_v1_warnings():
++            init_db()
++
++
++@pytest.fixture(autouse=True)
++def ignore_v1_deprecations(db):
++    with ignore_v1_warnings():
++        yield
++
++
++@pytest.fixture(scope="class", autouse=True)
++def ignore_v1_deprecations_in_class_setup():
++    with ignore_v1_warnings():
++        yield
++
++
++@contextmanager
++def ignore_v1_warnings():
++    msg = (
++        r"CTE(Manager|QuerySet) is deprecated.*"
++        r"|"
++        r"Use `django_cte\.with_cte\(.*\)` instead\."
++        r"|"
++        r"Use `django_cte\.CTE(\.recursive)? instead\."
++    )
++    with warnings.catch_warnings():
++        warnings.filterwarnings("ignore", message=msg, category=DeprecationWarning)
++        yield
+--- a/tests/test_v1/__init__.py
++++ b/tests/test_v1/__init__.py
+@@ -1,19 +1,5 @@
+-from unmagic import fixture
+-
+ from .. import ignore_v1_warnings
+ 
+ 
+-@fixture(autouse=__file__)
+-def ignore_v1_deprecations():
+-    with ignore_v1_warnings():
+-        yield
+-
+-
+-@fixture(autouse=__file__, scope="class")
+-def ignore_v1_deprecations_in_class_setup():
+-    with ignore_v1_warnings():
+-        yield
+-
+-
+ with ignore_v1_warnings():
+     from . import models  # noqa: F401
+--- /dev/null
++++ b/tests/test_v1/conftest.py
+@@ -0,0 +1,14 @@
++import pytest
++from .. import ignore_v1_warnings
++
++
++@pytest.fixture(autouse=True)
++def ignore_v1_deprecations():
++    with ignore_v1_warnings():
++        yield
++
++
++@pytest.fixture(scope="class", autouse=True)
++def ignore_v1_deprecations_in_class_setup():
++    with ignore_v1_warnings():
++        yield
diff -pruN 1.3.3-2/debian/patches/series 2.0.0-0ubuntu1/debian/patches/series
--- 1.3.3-2/debian/patches/series	2024-12-12 16:52:27.000000000 +0000
+++ 2.0.0-0ubuntu1/debian/patches/series	2025-08-13 18:13:52.000000000 +0000
@@ -1 +1 @@
-pytest.patch
+remove-unmagic.patch
diff -pruN 1.3.3-2/django_cte/__init__.py 2.0.0-0ubuntu1/django_cte/__init__.py
--- 1.3.3-2/django_cte/__init__.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/__init__.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,6 +1,4 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
+from .cte import CTE, with_cte, CTEManager, CTEQuerySet, With  # noqa
 
-from .cte import CTEManager, CTEQuerySet, With  # noqa
-
-__version__ = "1.3.3"
+__version__ = "2.0.0"
+__all__ = ["CTE", "with_cte"]
diff -pruN 1.3.3-2/django_cte/_deprecated.py 2.0.0-0ubuntu1/django_cte/_deprecated.py
--- 1.3.3-2/django_cte/_deprecated.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/_deprecated.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,138 @@
+try:
+    from warnings import deprecated
+except ImportError:
+    from warnings import warn
+
+    # Copied from Python 3.13, lightly modified for Python 3.9 compatibility.
+    # Can be removed when the oldest supported Python version is 3.13.
+    class deprecated:
+        """Indicate that a class, function or overload is deprecated.
+
+        When this decorator is applied to an object, the type checker
+        will generate a diagnostic on usage of the deprecated object.
+
+        Usage:
+
+            @deprecated("Use B instead")
+            class A:
+                pass
+
+            @deprecated("Use g instead")
+            def f():
+                pass
+
+            @overload
+            @deprecated("int support is deprecated")
+            def g(x: int) -> int: ...
+            @overload
+            def g(x: str) -> int: ...
+
+        The warning specified by *category* will be emitted at runtime
+        on use of deprecated objects. For functions, that happens on calls;
+        for classes, on instantiation and on creation of subclasses.
+        If the *category* is ``None``, no warning is emitted at runtime.
+        The *stacklevel* determines where the
+        warning is emitted. If it is ``1`` (the default), the warning
+        is emitted at the direct caller of the deprecated object; if it
+        is higher, it is emitted further up the stack.
+        Static type checker behavior is not affected by the *category*
+        and *stacklevel* arguments.
+
+        The deprecation message passed to the decorator is saved in the
+        ``__deprecated__`` attribute on the decorated object.
+        If applied to an overload, the decorator
+        must be after the ``@overload`` decorator for the attribute to
+        exist on the overload as returned by ``get_overloads()``.
+
+        See PEP 702 for details.
+
+        """
+        def __init__(
+            self,
+            message: str,
+            /,
+            *,
+            category=DeprecationWarning,
+            stacklevel=1,
+        ):
+            if not isinstance(message, str):
+                raise TypeError(
+                    f"Expected an object of type str for 'message', not {type(message).__name__!r}"
+                )
+            self.message = message
+            self.category = category
+            self.stacklevel = stacklevel
+
+        def __call__(self, arg, /):
+            # Make sure the inner functions created below don't
+            # retain a reference to self.
+            msg = self.message
+            category = self.category
+            stacklevel = self.stacklevel
+            if category is None:
+                arg.__deprecated__ = msg
+                return arg
+            elif isinstance(arg, type):
+                import functools
+                from types import MethodType
+
+                original_new = arg.__new__
+
+                @functools.wraps(original_new)
+                def __new__(cls, /, *args, **kwargs):
+                    if cls is arg:
+                        warn(msg, category=category, stacklevel=stacklevel + 1)
+                    if original_new is not object.__new__:
+                        return original_new(cls, *args, **kwargs)
+                    # Mirrors a similar check in object.__new__.
+                    elif cls.__init__ is object.__init__ and (args or kwargs):
+                        raise TypeError(f"{cls.__name__}() takes no arguments")
+                    else:
+                        return original_new(cls)
+
+                arg.__new__ = staticmethod(__new__)
+
+                original_init_subclass = arg.__init_subclass__
+                # We need slightly different behavior if __init_subclass__
+                # is a bound method (likely if it was implemented in Python)
+                if isinstance(original_init_subclass, MethodType):
+                    original_init_subclass = original_init_subclass.__func__
+
+                    @functools.wraps(original_init_subclass)
+                    def __init_subclass__(*args, **kwargs):
+                        warn(msg, category=category, stacklevel=stacklevel + 1)
+                        return original_init_subclass(*args, **kwargs)
+
+                    arg.__init_subclass__ = classmethod(__init_subclass__)
+                # Or otherwise, which likely means it's a builtin such as
+                # object's implementation of __init_subclass__.
+                else:
+                    @functools.wraps(original_init_subclass)
+                    def __init_subclass__(*args, **kwargs):
+                        warn(msg, category=category, stacklevel=stacklevel + 1)
+                        return original_init_subclass(*args, **kwargs)
+
+                    arg.__init_subclass__ = __init_subclass__
+
+                arg.__deprecated__ = __new__.__deprecated__ = msg
+                __init_subclass__.__deprecated__ = msg
+                return arg
+            elif callable(arg):
+                import functools
+                import inspect
+
+                @functools.wraps(arg)
+                def wrapper(*args, **kwargs):
+                    warn(msg, category=category, stacklevel=stacklevel + 1)
+                    return arg(*args, **kwargs)
+
+                if inspect.iscoroutinefunction(arg):
+                    wrapper = inspect.markcoroutinefunction(wrapper)
+
+                arg.__deprecated__ = wrapper.__deprecated__ = msg
+                return wrapper
+            else:
+                raise TypeError(
+                    "@deprecated decorator with non-None category must be applied to "
+                    f"a class or callable, not {arg!r}"
+                )
diff -pruN 1.3.3-2/django_cte/cte.py 2.0.0-0ubuntu1/django_cte/cte.py
--- 1.3.3-2/django_cte/cte.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/cte.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,19 +1,38 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
+from copy import copy
 
-from django.db.models import Manager
+from django.db.models import Manager, sql
+from django.db.models.expressions import Ref
 from django.db.models.query import Q, QuerySet, ValuesIterable
 from django.db.models.sql.datastructures import BaseTable
 
+from .jitmixin import jit_mixin
 from .join import QJoin, INNER
 from .meta import CTEColumnRef, CTEColumns
 from .query import CTEQuery
+from ._deprecated import deprecated
 
-__all__ = ["With", "CTEManager", "CTEQuerySet"]
+__all__ = ["CTE", "with_cte"]
 
 
-class With(object):
-    """Common Table Expression query object: `WITH ...`
+def with_cte(*ctes, select):
+    """Add Common Table Expression(s) (CTEs) to a model or queryset
+
+    :param *ctes: One or more CTE objects.
+    :param select: A model class, queryset, or CTE to use as the base
+        query to which CTEs are attached.
+    :returns: A queryset with the given CTE added to it.
+    """
+    if isinstance(select, CTE):
+        select = select.queryset()
+    elif not isinstance(select, QuerySet):
+        select = select._default_manager.all()
+    jit_mixin(select.query, CTEQuery)
+    select.query._with_ctes += ctes
+    return select
+
+
+class CTE:
+    """Common Table Expression
 
     :param queryset: A queryset to use as the body of the CTE.
     :param name: Optional name parameter for the CTE (default: "cte").
@@ -43,7 +62,7 @@ class With(object):
 
     @classmethod
     def recursive(cls, make_cte_queryset, name="cte", materialized=False):
-        """Recursive Common Table Expression: `WITH RECURSIVE ...`
+        """Recursive Common Table Expression
 
         :param make_cte_queryset: Function taking a single argument (a
         not-yet-fully-constructed cte object) and returning a `QuerySet`
@@ -60,10 +79,11 @@ class With(object):
     def join(self, model_or_queryset, *filter_q, **filter_kw):
         """Join this CTE to the given model or queryset
 
-        This CTE will be refernced by the returned queryset, but the
+        This CTE will be referenced by the returned queryset, but the
+
         corresponding `WITH ...` statement will not be prepended to the
-        queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
-        achieve that outcome.
+        queryset's SQL output; use `with_cte(cte, select=cte.join(...))`
+        to achieve that outcome.
 
         :param model_or_queryset: Model class or queryset to which the
         CTE should be joined.
@@ -98,53 +118,72 @@ class With(object):
 
         This CTE will be referenced by the returned queryset, but the
         corresponding `WITH ...` statement will not be prepended to the
-        queryset's SQL output; use `<CTEQuerySet>.with_cte(cte)` to
-        achieve that outcome.
+        queryset's SQL output; use `with_cte(cte, select=cte)` to do
+        that.
 
         :returns: A queryset.
         """
         cte_query = self.query
         qs = cte_query.model._default_manager.get_queryset()
 
-        query = CTEQuery(cte_query.model)
+        query = jit_mixin(sql.Query(cte_query.model), CTEQuery)
         query.join(BaseTable(self.name, None))
         query.default_cols = cte_query.default_cols
         query.deferred_loading = cte_query.deferred_loading
+        if cte_query.values_select:
+            query.set_values(cte_query.values_select)
+            qs._iterable_class = ValuesIterable
+        for alias in getattr(cte_query, "selected", None) or ():
+            if alias not in cte_query.annotations:
+                col = Ref(alias, cte_query.resolve_ref(alias))
+                query.add_annotation(col, alias)
         if cte_query.annotations:
             for alias, value in cte_query.annotations.items():
                 col = CTEColumnRef(alias, self.name, value.output_field)
                 query.add_annotation(col, alias)
-        if cte_query.values_select:
-            query.set_values(cte_query.values_select)
-            qs._iterable_class = ValuesIterable
         query.annotation_select_mask = cte_query.annotation_select_mask
 
         qs.query = query
         return qs
 
     def _resolve_ref(self, name):
+        selected = getattr(self.query, "selected", None)
+        if selected and name in selected and name not in self.query.annotations:
+            return Ref(name, self.query.resolve_ref(name))
         return self.query.resolve_ref(name)
 
+    def resolve_expression(self, *args, **kw):
+        if self.query is None:
+            raise ValueError("Cannot resolve recursive CTE without a query.")
+        clone = copy(self)
+        clone.query = clone.query.resolve_expression(*args, **kw)
+        return clone
+
+
+@deprecated("Use `django_cte.CTE` instead.")
+class With(CTE):
 
+    @staticmethod
+    @deprecated("Use `django_cte.CTE.recursive` instead.")
+    def recursive(*args, **kw):
+        return CTE.recursive(*args, **kw)
+
+
+@deprecated("CTEQuerySet is deprecated. "
+            "CTEs can now be applied to any queryset using `with_cte()`")
 class CTEQuerySet(QuerySet):
     """QuerySet with support for Common Table Expressions"""
 
     def __init__(self, model=None, query=None, using=None, hints=None):
         # Only create an instance of a Query if this is the first invocation in
         # a query chain.
-        if query is None:
-            query = CTEQuery(model)
         super(CTEQuerySet, self).__init__(model, query, using, hints)
+        jit_mixin(self.query, CTEQuery)
 
+    @deprecated("Use `django_cte.with_cte(cte, select=...)` instead.")
     def with_cte(self, cte):
-        """Add a Common Table Expression to this queryset
-
-        The CTE `WITH ...` clause will be added to the queryset's SQL
-        output (after other CTEs that have already been added) so it
-        can be referenced in annotations, filters, etc.
-        """
         qs = self._clone()
-        qs.query._with_ctes.append(cte)
+        qs.query._with_ctes += cte,
         return qs
 
     def as_manager(cls):
@@ -157,6 +196,8 @@ class CTEQuerySet(QuerySet):
     as_manager = classmethod(as_manager)
 
 
+@deprecated("CTEMAnager is deprecated. "
+            "CTEs can now be applied to any queryset using `with_cte()`")
 class CTEManager(Manager.from_queryset(CTEQuerySet)):
     """Manager for models that perform CTE queries"""
 
diff -pruN 1.3.3-2/django_cte/expressions.py 2.0.0-0ubuntu1/django_cte/expressions.py
--- 1.3.3-2/django_cte/expressions.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/expressions.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,50 +0,0 @@
-import django
-from django.db.models import Subquery
-
-
-class CTESubqueryResolver(object):
-
-    def __init__(self, annotation):
-        self.annotation = annotation
-
-    def resolve_expression(self, *args, **kw):
-        # source: django.db.models.expressions.Subquery.resolve_expression
-        # --- begin copied code (lightly adapted) --- #
-
-        # Need to recursively resolve these.
-        def resolve_all(child):
-            if hasattr(child, 'children'):
-                [resolve_all(_child) for _child in child.children]
-            if hasattr(child, 'rhs'):
-                child.rhs = resolve(child.rhs)
-
-        def resolve(child):
-            if hasattr(child, 'resolve_expression'):
-                resolved = child.resolve_expression(*args, **kw)
-                # Add table alias to the parent query's aliases to prevent
-                # quoting.
-                if hasattr(resolved, 'alias') and \
-                        resolved.alias != resolved.target.model._meta.db_table:
-                    get_query(clone).external_aliases.add(resolved.alias)
-                return resolved
-            return child
-
-        # --- end copied code --- #
-
-        if django.VERSION < (3, 0):
-            def get_query(clone):
-                return clone.queryset.query
-        else:
-            def get_query(clone):
-                return clone.query
-
-        # NOTE this uses the old (pre-Django 3) way of resolving.
-        # Should a different technique should be used on Django 3+?
-        clone = self.annotation.resolve_expression(*args, **kw)
-        if isinstance(self.annotation, Subquery):
-            for cte in getattr(get_query(clone), '_with_ctes', []):
-                resolve_all(cte.query.where)
-                for key, value in cte.query.annotations.items():
-                    if isinstance(value, Subquery):
-                        cte.query.annotations[key] = resolve(value)
-        return clone
diff -pruN 1.3.3-2/django_cte/jitmixin.py 2.0.0-0ubuntu1/django_cte/jitmixin.py
--- 1.3.3-2/django_cte/jitmixin.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/jitmixin.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,28 @@
+def jit_mixin(obj, mixin):
+    """Apply mixin to object and return the object"""
+    if not isinstance(obj, mixin):
+        obj.__class__ = jit_mixin_type(obj.__class__, mixin)
+    return obj
+
+
+def jit_mixin_type(base, *mixins):
+    assert not issubclass(base, mixins), (base, mixins)
+    mixed = _mixin_cache.get((base, mixins))
+    if mixed is None:
+        prefix = "".join(m._jit_mixin_prefix for m in mixins)
+        name = f"{prefix}{base.__name__}"
+        mixed = _mixin_cache[(base, mixins)] = type(name, (*mixins, base), {
+            "_jit_mixin_base": getattr(base, "_jit_mixin_base", base),
+            "_jit_mixins": mixins + getattr(base, "_jit_mixins", ()),
+        })
+    return mixed
+
+
+_mixin_cache = {}
+
+
+class JITMixin:
+
+    def __reduce__(self):
+        # make JITMixin classes pickleable
+        return (jit_mixin_type, (self._jit_mixin_base, *self._jit_mixins))
diff -pruN 1.3.3-2/django_cte/join.py 2.0.0-0ubuntu1/django_cte/join.py
--- 1.3.3-2/django_cte/join.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/join.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,9 +1,7 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
 from django.db.models.sql.constants import INNER
 
 
-class QJoin(object):
+class QJoin:
     """Join clause with join condition from Q object clause
 
     :param parent_alias: Alias of parent table.
diff -pruN 1.3.3-2/django_cte/meta.py 2.0.0-0ubuntu1/django_cte/meta.py
--- 1.3.3-2/django_cte/meta.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/meta.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,12 +1,9 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 import weakref
 
 from django.db.models.expressions import Col, Expression
 
 
-class CTEColumns(object):
+class CTEColumns:
 
     def __init__(self, cte):
         self._cte = weakref.ref(cte)
@@ -90,7 +87,7 @@ class CTEColumnRef(Expression):
             clone._alias = self._alias or query.table_map.get(
                 self.cte_name, [self.cte_name])[0]
             return clone
-        return super(CTEColumnRef, self).resolve_expression(
+        return super().resolve_expression(
             query, allow_joins, reuse, summarize, for_save)
 
     def relabeled_clone(self, change_map):
@@ -98,7 +95,7 @@ class CTEColumnRef(Expression):
             self.cte_name not in change_map
             and self._alias not in change_map
         ):
-            return super(CTEColumnRef, self).relabeled_clone(change_map)
+            return super().relabeled_clone(change_map)
 
         clone = self.copy()
         if self.cte_name in change_map:
diff -pruN 1.3.3-2/django_cte/query.py 2.0.0-0ubuntu1/django_cte/query.py
--- 1.3.3-2/django_cte/query.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/query.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,225 +1,168 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 import django
 from django.core.exceptions import EmptyResultSet
-from django.db import connections
-from django.db.models.sql import DeleteQuery, Query, UpdateQuery
-from django.db.models.sql.compiler import (
-    SQLCompiler,
-    SQLDeleteCompiler,
-    SQLUpdateCompiler,
-)
 from django.db.models.sql.constants import LOUTER
-from django.db.models.sql.where import ExtraWhere, WhereNode
 
-from .expressions import CTESubqueryResolver
+from .jitmixin import JITMixin, jit_mixin
 from .join import QJoin
 
+# NOTE: it is currently not possible to execute delete queries that
+# reference CTEs without patching `QuerySet.delete` (Django method)
+# to call `self.query.chain(sql.DeleteQuery)` instead of
+# `sql.DeleteQuery(self.model)`
+
+
+class CTEQuery(JITMixin):
+    """A Query mixin that processes SQL compilation through a CTE compiler"""
+    _jit_mixin_prefix = "CTE"
+    _with_ctes = ()
+
+    @property
+    def combined_queries(self):
+        return self.__dict__.get("combined_queries", ())
 
-class CTEQuery(Query):
-    """A Query which processes SQL compilation through the CTE compiler"""
-
-    def __init__(self, *args, **kwargs):
-        super(CTEQuery, self).__init__(*args, **kwargs)
-        self._with_ctes = []
-
-    def combine(self, other, connector):
-        if other._with_ctes:
-            if self._with_ctes:
-                raise TypeError("cannot merge queries with CTEs on both sides")
-            self._with_ctes = other._with_ctes[:]
-        return super(CTEQuery, self).combine(other, connector)
-
-    def get_compiler(self, using=None, connection=None, *args, **kwargs):
-        """ Overrides the Query method get_compiler in order to return
-            a CTECompiler.
-        """
-        # Copy the body of this method from Django except the final
-        # return statement. We will ignore code coverage for this.
-        if using is None and connection is None:  # pragma: no cover
-            raise ValueError("Need either using or connection")
-        if using:
-            connection = connections[using]
-        # Check that the compiler will be able to execute the query
-        for alias, aggregate in self.annotation_select.items():
-            connection.ops.check_expression_support(aggregate)
-        # Instantiate the custom compiler.
-        klass = COMPILER_TYPES.get(self.__class__, CTEQueryCompiler)
-        return klass(self, connection, using, *args, **kwargs)
-
-    def add_annotation(self, annotation, *args, **kw):
-        annotation = CTESubqueryResolver(annotation)
-        super(CTEQuery, self).add_annotation(annotation, *args, **kw)
-
-    def __chain(self, _name, klass=None, *args, **kwargs):
-        klass = QUERY_TYPES.get(klass, self.__class__)
-        clone = getattr(super(CTEQuery, self), _name)(klass, *args, **kwargs)
-        clone._with_ctes = self._with_ctes[:]
+    @combined_queries.setter
+    def combined_queries(self, queries):
+        ctes = []
+        seen = {cte.name: cte for cte in self._with_ctes}
+        for query in queries:
+            for cte in getattr(query, "_with_ctes", ()):
+                if seen.get(cte.name) is cte:
+                    continue
+                if cte.name in seen:
+                    raise ValueError(
+                        f"Found two or more CTEs named '{cte.name}'. "
+                        "Hint: assign a unique name to each CTE."
+                    )
+                ctes.append(cte)
+                seen[cte.name] = cte
+
+        if seen:
+            def without_ctes(query):
+                if getattr(query, "_with_ctes", None):
+                    query = query.clone()
+                    del query._with_ctes
+                return query
+
+            self._with_ctes += tuple(ctes)
+            queries = tuple(without_ctes(q) for q in queries)
+        self.__dict__["combined_queries"] = queries
+
+    def resolve_expression(self, *args, **kwargs):
+        clone = super().resolve_expression(*args, **kwargs)
+        clone._with_ctes = tuple(
+            cte.resolve_expression(*args, **kwargs)
+            for cte in clone._with_ctes
+        )
         return clone
 
-    if django.VERSION < (2, 0):
-        def clone(self, klass=None, *args, **kwargs):
-            return self.__chain("clone", klass, *args, **kwargs)
-
-    else:
-        def chain(self, klass=None):
-            return self.__chain("chain", klass)
+    def get_compiler(self, *args, **kwargs):
+        return jit_mixin(super().get_compiler(*args, **kwargs), CTECompiler)
 
+    def chain(self, klass=None):
+        clone = jit_mixin(super().chain(klass), CTEQuery)
+        clone._with_ctes = self._with_ctes
+        return clone
 
-class CTECompiler(object):
-
-    @classmethod
-    def generate_sql(cls, connection, query, as_sql):
-        if query.combinator:
-            return as_sql()
 
-        ctes = []
-        params = []
-        for cte in query._with_ctes:
-            if django.VERSION > (4, 2):
-                _ignore_with_col_aliases(cte.query)
-
-            alias = query.alias_map.get(cte.name)
-            should_elide_empty = (
-                    not isinstance(alias, QJoin) or alias.join_type != LOUTER
+def generate_cte_sql(connection, query, as_sql):
+    if not query._with_ctes:
+        return as_sql()
+
+    ctes = []
+    params = []
+    for cte in query._with_ctes:
+        if django.VERSION > (4, 2):
+            _ignore_with_col_aliases(cte.query)
+
+        alias = query.alias_map.get(cte.name)
+        should_elide_empty = (
+                not isinstance(alias, QJoin) or alias.join_type != LOUTER
+        )
+
+        compiler = cte.query.get_compiler(
+            connection=connection, elide_empty=should_elide_empty
+        )
+
+        qn = compiler.quote_name_unless_alias
+        try:
+            cte_sql, cte_params = compiler.as_sql()
+        except EmptyResultSet:
+            # If the CTE raises an EmptyResultSet the SqlCompiler still
+            # needs to know the information about this base compiler
+            # like, col_count and klass_info.
+            as_sql()
+            raise
+        template = get_cte_query_template(cte)
+        ctes.append(template.format(name=qn(cte.name), query=cte_sql))
+        params.extend(cte_params)
+
+    explain_attribute = "explain_info"
+    explain_info = getattr(query, explain_attribute, None)
+    explain_format = getattr(explain_info, "format", None)
+    explain_options = getattr(explain_info, "options", {})
+
+    explain_query_or_info = getattr(query, explain_attribute, None)
+    sql = []
+    if explain_query_or_info:
+        sql.append(
+            connection.ops.explain_query_prefix(
+                explain_format,
+                **explain_options
             )
-
-            if django.VERSION >= (4, 0):
-                compiler = cte.query.get_compiler(
-                    connection=connection, elide_empty=should_elide_empty
-                )
-            else:
-                compiler = cte.query.get_compiler(connection=connection)
-
-            qn = compiler.quote_name_unless_alias
-            try:
-                cte_sql, cte_params = compiler.as_sql()
-            except EmptyResultSet:
-                if django.VERSION < (4, 0) and not should_elide_empty:
-                    # elide_empty is not available prior to Django 4.0. The
-                    # below behavior emulates the logic of it, rebuilding
-                    # the CTE query with a WHERE clause that is always false
-                    # but that the SqlCompiler cannot optimize away. This is
-                    # only required for left outer joins, as standard inner
-                    # joins should be optimized and raise the EmptyResultSet
-                    query = cte.query.copy()
-                    query.where = WhereNode([ExtraWhere(["1 = 0"], [])])
-                    compiler = query.get_compiler(connection=connection)
-                    cte_sql, cte_params = compiler.as_sql()
-                else:
-                    # If the CTE raises an EmptyResultSet the SqlCompiler still
-                    # needs to know the information about this base compiler
-                    # like, col_count and klass_info.
-                    as_sql()
-                    raise
-            template = cls.get_cte_query_template(cte)
-            ctes.append(template.format(name=qn(cte.name), query=cte_sql))
-            params.extend(cte_params)
-
-        # Required due to breaking change in django commit
-        #     fc91ea1e50e5ef207f0f291b3f6c1942b10db7c7
-        if django.VERSION >= (4, 0):
-            explain_attribute = "explain_info"
-            explain_info = getattr(query, explain_attribute, None)
-            explain_format = getattr(explain_info, "format", None)
-            explain_options = getattr(explain_info, "options", {})
-        else:
-            explain_attribute = "explain_query"
-            explain_format = getattr(query, "explain_format", None)
-            explain_options = getattr(query, "explain_options", {})
-
-        explain_query_or_info = getattr(query, explain_attribute, None)
-        sql = []
-        if explain_query_or_info:
-            sql.append(
-                connection.ops.explain_query_prefix(
-                    explain_format,
-                    **explain_options
-                )
-            )
-            # this needs to get set to None so that the base as_sql() doesn't
-            # insert the EXPLAIN statement where it would end up between the
-            # WITH ... clause and the final SELECT
-            setattr(query, explain_attribute, None)
-
-        if ctes:
-            # Always use WITH RECURSIVE
-            # https://www.postgresql.org/message-id/13122.1339829536%40sss.pgh.pa.us
-            sql.extend(["WITH RECURSIVE", ", ".join(ctes)])
-        base_sql, base_params = as_sql()
-
-        if explain_query_or_info:
-            setattr(query, explain_attribute, explain_query_or_info)
-
-        sql.append(base_sql)
-        params.extend(base_params)
-        return " ".join(sql), tuple(params)
-
-    @classmethod
-    def get_cte_query_template(cls, cte):
-        if cte.materialized:
-            return "{name} AS MATERIALIZED ({query})"
-        return "{name} AS ({query})"
-
-
-class CTEUpdateQuery(UpdateQuery, CTEQuery):
-    pass
-
-
-class CTEDeleteQuery(DeleteQuery, CTEQuery):
-    pass
-
-
-QUERY_TYPES = {
-    Query: CTEQuery,
-    UpdateQuery: CTEUpdateQuery,
-    DeleteQuery: CTEDeleteQuery,
-}
+        )
+        # this needs to get set to None so that the base as_sql() doesn't
+        # insert the EXPLAIN statement where it would end up between the
+        # WITH ... clause and the final SELECT
+        setattr(query, explain_attribute, None)
+
+    if ctes:
+        # Always use WITH RECURSIVE
+        # https://www.postgresql.org/message-id/13122.1339829536%40sss.pgh.pa.us
+        sql.extend(["WITH RECURSIVE", ", ".join(ctes)])
+    base_sql, base_params = as_sql()
+
+    if explain_query_or_info:
+        setattr(query, explain_attribute, explain_query_or_info)
+
+    sql.append(base_sql)
+    params.extend(base_params)
+    return " ".join(sql), tuple(params)
+
+
+def get_cte_query_template(cte):
+    if cte.materialized:
+        return "{name} AS MATERIALIZED ({query})"
+    return "{name} AS ({query})"
 
 
 def _ignore_with_col_aliases(cte_query):
     if getattr(cte_query, "combined_queries", None):
-        for query in cte_query.combined_queries:
-            query.ignore_with_col_aliases = True
-
-
-class CTEQueryCompiler(SQLCompiler):
-
-    def as_sql(self, *args, **kwargs):
-        def _as_sql():
-            return super(CTEQueryCompiler, self).as_sql(*args, **kwargs)
-        return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
-
-    def get_select(self, **kw):
-        if kw.get("with_col_aliases") \
-                and getattr(self.query, "ignore_with_col_aliases", False):
-            kw.pop("with_col_aliases")
-        return super().get_select(**kw)
+        cte_query.combined_queries = tuple(
+            jit_mixin(q, NoAliasQuery) for q in cte_query.combined_queries
+        )
 
 
-class CTEUpdateQueryCompiler(SQLUpdateCompiler):
+class CTECompiler(JITMixin):
+    """Mixin for django.db.models.sql.compiler.SQLCompiler"""
+    _jit_mixin_prefix = "CTE"
 
     def as_sql(self, *args, **kwargs):
         def _as_sql():
-            return super(CTEUpdateQueryCompiler, self).as_sql(*args, **kwargs)
-        return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
+            return super(CTECompiler, self).as_sql(*args, **kwargs)
+        return generate_cte_sql(self.connection, self.query, _as_sql)
 
 
-class CTEDeleteQueryCompiler(SQLDeleteCompiler):
+class NoAliasQuery(JITMixin):
+    """Mixin for django.db.models.sql.compiler.Query"""
+    _jit_mixin_prefix = "NoAlias"
 
-    # NOTE: it is currently not possible to execute delete queries that
-    # reference CTEs without patching `QuerySet.delete` (Django method)
-    # to call `self.query.chain(sql.DeleteQuery)` instead of
-    # `sql.DeleteQuery(self.model)`
+    def get_compiler(self, *args, **kwargs):
+        return jit_mixin(super().get_compiler(*args, **kwargs), NoAliasCompiler)
 
-    def as_sql(self, *args, **kwargs):
-        def _as_sql():
-            return super(CTEDeleteQueryCompiler, self).as_sql(*args, **kwargs)
-        return CTECompiler.generate_sql(self.connection, self.query, _as_sql)
 
+class NoAliasCompiler(JITMixin):
+    """Mixin for django.db.models.sql.compiler.SQLCompiler"""
+    _jit_mixin_prefix = "NoAlias"
 
-COMPILER_TYPES = {
-    CTEUpdateQuery: CTEUpdateQueryCompiler,
-    CTEDeleteQuery: CTEDeleteQueryCompiler,
-}
+    def get_select(self, *, with_col_aliases=False, **kw):
+        return super().get_select(**kw)
diff -pruN 1.3.3-2/django_cte/raw.py 2.0.0-0ubuntu1/django_cte/raw.py
--- 1.3.3-2/django_cte/raw.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/django_cte/raw.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,7 +1,3 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-
 def raw_cte_sql(sql, params, refs):
     """Raw CTE SQL
 
@@ -11,14 +7,14 @@ def raw_cte_sql(sql, params, refs):
     :returns: Object that can be passed to `With`.
     """
 
-    class raw_cte_ref(object):
+    class raw_cte_ref:
         def __init__(self, output_field):
             self.output_field = output_field
 
         def get_source_expressions(self):
             return []
 
-    class raw_cte_compiler(object):
+    class raw_cte_compiler:
 
         def __init__(self, connection):
             self.connection = connection
@@ -29,8 +25,8 @@ def raw_cte_sql(sql, params, refs):
         def quote_name_unless_alias(self, name):
             return self.connection.ops.quote_name(name)
 
-    class raw_cte_queryset(object):
-        class query(object):
+    class raw_cte_queryset:
+        class query:
             @staticmethod
             def get_compiler(connection, *, elide_empty=None):
                 return raw_cte_compiler(connection)
diff -pruN 1.3.3-2/docs/index.md 2.0.0-0ubuntu1/docs/index.md
--- 1.3.3-2/docs/index.md	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/docs/index.md	2025-06-16 15:26:31.000000000 +0000
@@ -8,48 +8,32 @@ for the duration of the query it is atta
 expressions to be attached to normal Django ORM queries.
 
 
-## Prerequisite: A Model with a "CTEManager"
-
-The custom manager class, `CTEManager`, constructs `CTEQuerySet`s, which have
-all of the same features as normal `QuerySet`s and also support CTE queries.
-
-```py
-from django_cte import CTEManager
-
-class Order(Model):
-    objects = CTEManager()
-    id = AutoField(primary_key=True)
-    region = ForeignKey("Region", on_delete=CASCADE)
-    amount = IntegerField(default=0)
-
-    class Meta:
-        db_table = "orders"
-```
-
-
 ## Simple Common Table Expressions
 
-Simple CTEs are constructed using `With(...)`. A CTE can be joined to a model or
-other `CTEQuerySet` using its `join(...)` method, which creates a new queryset
-with a `JOIN` and `ON` condition. Finally, the CTE is added to the resulting
-queryset using `with_cte(cte)`, which adds the `WITH` expression before the
-main `SELECT` query.
+See [Appendix A](#appendix-a-model-definitions-used-in-sample-code) for model
+definitions used in sample code.
+
+Simple CTEs are constructed using `CTE(...)`. A CTE is added to a queryset using
+`with_cte(cte, select=queryset)`, which adds the `WITH` expression before the
+main `SELECT` query. A CTE can be joined to a model or other `QuerySet` using
+its `<CTE>.join(...)` method, which creates a new queryset with a `JOIN` and
+`ON` condition.
 
 ```py
-from django_cte import With
+from django_cte import CTE, with_cte
 
-cte = With(
+cte = CTE(
     Order.objects
     .values("region_id")
     .annotate(total=Sum("amount"))
 )
 
-orders = (
-    # FROM orders INNER JOIN cte ON orders.region_id = cte.region_id
-    cte.join(Order, region=cte.col.region_id)
+orders = with_cte(
+    # WITH cte ...
+    cte,
 
-    # Add `WITH ...` before `SELECT ... FROM orders ...`
-    .with_cte(cte)
+    # SELECT ... FROM orders INNER JOIN cte ON orders.region_id = cte.region_id
+    select=cte.join(Order, region=cte.col.region_id)
 
     # Annotate each Order with a "region_total"
     .annotate(region_total=cte.col.total)
@@ -78,7 +62,7 @@ FROM "orders"
 INNER JOIN "cte" ON "orders"."region_id" = "cte"."region_id"
 ```
 
-The `orders` query is a query set containing annotated `Order` objects, just as
+The `orders` query is a queryset containing annotated `Order` objects, just as
 you would get from a query like `Order.objects.annotate(region_total=...)`. Each
 `Order` object will be annotated with a `region_total` attribute, which is
 populated with the value of the corresponding total from the joined CTE query.
@@ -93,19 +77,9 @@ recursive CTEs to be included in the WIT
 ## Recursive Common Table Expressions
 
 Recursive CTE queries allow fundamentally new types of queries that are
-not otherwise possible. First, a model for the example.
-
-```py
-class Region(Model):
-    objects = CTEManager()
-    name = TextField(primary_key=True)
-    parent = ForeignKey("self", null=True, on_delete=CASCADE)
-
-    class Meta:
-        db_table = "region"
-```
+not otherwise possible.
 
-Recursive CTEs are constructed using `With.recursive()`, which takes as its
+Recursive CTEs are constructed using `CTE.recursive()`, which takes as its
 first argument a function that constructs and returns a recursive query.
 Recursive queries have two elements: first a non-recursive query element, and
 second a recursive query element. The second is typically attached to the first
@@ -133,11 +107,11 @@ def make_regions_cte(cte):
         all=True,
     )
 
-cte = With.recursive(make_regions_cte)
+cte = CTE.recursive(make_regions_cte)
 
-regions = (
-    cte.join(Region, name=cte.col.name)
-    .with_cte(cte)
+regions = with_cte(
+    cte,
+    select=cte.join(Region, name=cte.col.name)
     .annotate(
         path=cte.col.path,
         depth=cte.col.depth,
@@ -184,9 +158,9 @@ ORDER BY "path" ASC
 ## Named Common Table Expressions
 
 It is possible to add more than one CTE to a query. To do this, each CTE must
-have a unique name. `With(queryset)` returns a CTE with the name `'cte'` by
-default, but that can be overridden: `With(queryset, name='custom')` or
-`With.recursive(make_queryset, name='custom')`. This allows each CTE to be
+have a unique name. `CTE(queryset)` returns a CTE with the name `'cte'` by
+default, but that can be overridden: `CTE(queryset, name='custom')` or
+`CTE.recursive(make_queryset, name='custom')`. This allows each CTE to be
 referenced uniquely within a single query.
 
 Also note that a CTE may reference other CTEs in the same query.
@@ -208,9 +182,9 @@ def make_root_mapping(rootmap):
         ),
         all=True,
     )
-rootmap = With.recursive(make_root_mapping, name="rootmap")
+rootmap = CTE.recursive(make_root_mapping, name="rootmap")
 
-totals = With(
+totals = CTE(
     rootmap.join(Order, region_id=rootmap.col.name)
     .values(
         root=rootmap.col.root,
@@ -221,11 +195,12 @@ totals = With(
     name="totals",
 )
 
-root_regions = (
-    totals.join(Region, name=totals.col.root)
-    # Important: add both CTEs to the final query
-    .with_cte(rootmap)
-    .with_cte(totals)
+root_regions = with_cte(
+    # Important: add both CTEs to the query
+    rootmap,
+    totals,
+
+    select=totals.join(Region, name=totals.col.root)
     .annotate(
         # count of orders in this region and all subregions
         orders_count=totals.col.orders_count,
@@ -276,16 +251,16 @@ INNER JOIN "totals" ON "region"."name" =
 
 Sometimes it is useful to construct queries where the final `FROM` clause
 contains only common table expression(s). This is possible with
-`With(...).queryset()`.
+`CTE(...).queryset()`.
 
 Each returned row may be a model object:
 
 ```py
-cte = With(
+cte = CTE(
     Order.objects
     .annotate(region_parent=F("region__parent_id")),
 )
-orders = cte.queryset().with_cte(cte)
+orders = with_cte(cte, select=cte.queryset())
 ```
 
 And the resulting SQL:
@@ -311,7 +286,7 @@ FROM "cte"
 It is also possible to do the same with `values(...)` queries:
 
 ```py
-cte = With(
+cte = CTE(
     Order.objects
     .values(
         "region_id",
@@ -319,7 +294,7 @@ cte = With(
     )
     .distinct()
 )
-values = cte.queryset().with_cte(cte).filter(region_parent__isnull=False)
+values = with_cte(cte, select=cte).filter(region_parent__isnull=False)
 ```
 
 Which produces this SQL:
@@ -339,55 +314,30 @@ FROM "cte"
 WHERE "cte"."region_parent" IS NOT NULL
 ```
 
-
-## Custom QuerySets and Managers
-
-Custom `QuerySet`s that will be used in CTE queries should be derived from
-`CTEQuerySet`.
-
-```py
-class LargeOrdersQuerySet(CTEQuerySet):
-    def big_amounts(self):
-        return self.filter(amount__gt=100)
-
-
-class Order(Model):
-    amount = models.IntegerField()
-    large = LargeOrdersQuerySet.as_manager()
-```
-
-Custom `CTEQuerySet`s can also be used with custom `CTEManager`s.
-
-```py
-class CustomManager(CTEManager):
-    ...
-
-
-class Order(Model):
-    large = CustomManager.from_queryset(LargeOrdersQuerySet)()
-    objects = CustomManager()
-```
+You may have noticed that when a CTE is passed to the `select=...` argument as
+in `with_cte(cte, select=cte)`, the `.queryset()` call is optional and may be
+omitted.
 
 
 ## Experimental: Left Outer Join
 
 Django does not provide precise control over joins, but there is an experimental
 way to perform a `LEFT OUTER JOIN` with a CTE query using the `_join_type`
-keyword argument of `With.join(...)`.
+keyword argument of `CTE.join(...)`.
 
 ```py
 from django.db.models.sql.constants import LOUTER
 
-totals = With(
+totals = CTE(
     Order.objects
     .values("region_id")
     .annotate(total=Sum("amount"))
     .filter(total__gt=100)
 )
-orders = (
-    totals
+orders = with_cte(
+    totals,
+    select=totals
     .join(Order, region=totals.col.region_id, _join_type=LOUTER)
-    .with_cte(totals)
     .annotate(region_total=totals.col.total)
 )
 ```
@@ -420,12 +370,13 @@ produce the desired SQL.
 
 ## Materialized CTE
 
-Both PostgreSQL 12+ and sqlite 3.35+ supports `MATERIALIZED` keyword for CTE queries.
-To enforce using of this keyword add `materialized` as a parameter of `With(..., materialized=True)`.
+Both PostgreSQL 12+ and sqlite 3.35+ supports `MATERIALIZED` keyword for CTE
+queries. To enforce usage of this keyword add `materialized` as a parameter of
+`CTE(..., materialized=True)`.
 
 
 ```py
-cte = With(
+cte = CTE(
     Order.objects.values('id'),
     materialized=True
 )
@@ -457,7 +408,7 @@ A short example:
 from django.db.models import IntegerField, TextField
 from django_cte.raw import raw_cte_sql
 
-cte = With(raw_cte_sql(
+cte = CTE(raw_cte_sql(
     """
     SELECT region_id, AVG(amount) AS avg_order
     FROM orders
@@ -470,11 +421,11 @@ cte = With(raw_cte_sql(
         "avg_order": IntegerField(),
     },
 ))
-moon_avg = (
-    cte
+moon_avg = with_cte(
+    cte,
+    select=cte
     .join(Region, name=cte.col.region_id)
     .annotate(avg_order=cte.col.avg_order)
-    .with_cte(cte)
 )
 ```
 
@@ -504,6 +455,36 @@ prevent SQL injection attacks.
 A few more advanced techniques as well as example query results can be found
 in the tests:
 
-- [`test_cte.py`](https://github.com/dimagi/django-cte/blob/master/tests/test_cte.py)
-- [`test_recursive.py`](https://github.com/dimagi/django-cte/blob/master/tests/test_recursive.py)
-- [`test_raw.py`](https://github.com/dimagi/django-cte/blob/master/tests/test_raw.py)
+- [`test_cte.py`](https://github.com/dimagi/django-cte/blob/main/tests/test_cte.py)
+- [`test_recursive.py`](https://github.com/dimagi/django-cte/blob/main/tests/test_recursive.py)
+- [`test_raw.py`](https://github.com/dimagi/django-cte/blob/main/tests/test_raw.py)
+
+
+## Appendix A: Model definitions used in sample code
+
+```py
+class Order(Model):
+    id = AutoField(primary_key=True)
+    region = ForeignKey("Region", on_delete=CASCADE)
+    amount = IntegerField(default=0)
+
+    class Meta:
+        db_table = "orders"
+
+
+class Region(Model):
+    name = TextField(primary_key=True)
+    parent = ForeignKey("self", null=True, on_delete=CASCADE)
+
+    class Meta:
+        db_table = "region"
+```
+
+
+## Appendix B: django-cte v1 documentation (DEPRECATED)
+
+The syntax for constructing CTE queries changed slightly in django-cte 2.0. The
+most important change is that a custom model manager is no longer required on
+models used to construct CTE queries. The documentation has been updated to use
+v2 syntax, but the [documentation for v1](https://github.com/dimagi/django-cte/blob/v1.3.3/docs/index.md)
+can be found on Github if needed.
diff -pruN 1.3.3-2/pkg-requires.txt 2.0.0-0ubuntu1/pkg-requires.txt
--- 1.3.3-2/pkg-requires.txt	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/pkg-requires.txt	1970-01-01 00:00:00.000000000 +0000
@@ -1,3 +0,0 @@
-setuptools>=38.6.0
-twine>=1.11.0
-wheel>=0.31.0
diff -pruN 1.3.3-2/pyproject.toml 2.0.0-0ubuntu1/pyproject.toml
--- 1.3.3-2/pyproject.toml	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/pyproject.toml	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,54 @@
+[project]
+name = "django-cte"
+description = "Common Table Expressions (CTE) for Django"
+authors = [{name = "Daniel Miller", email = "millerdev@gmail.com"}]
+license = {file = "LICENSE"}
+readme = {file = "README.md", content-type = "text/markdown"}
+dynamic = ["version"]
+requires-python = ">= 3.9"
+# Python and Django versions are read from this file by GitHub Actions.
+# Precise formatting is important.
+classifiers = [
+    "Development Status :: 5 - Production/Stable",
+    'Environment :: Web Environment',
+    'Framework :: Django',
+    'Intended Audience :: Developers',
+    'License :: OSI Approved :: BSD License',
+    'Operating System :: OS Independent',
+    'Programming Language :: Python',
+    'Programming Language :: Python :: 3',
+    'Programming Language :: Python :: 3.9',
+    'Programming Language :: Python :: 3.10',
+    'Programming Language :: Python :: 3.11',
+    'Programming Language :: Python :: 3.12',
+    'Programming Language :: Python :: 3.13',
+    'Programming Language :: Python :: 3.14',
+    'Framework :: Django',
+    'Framework :: Django :: 4',
+    'Framework :: Django :: 4.2',
+    'Framework :: Django :: 5',
+    'Framework :: Django :: 5.1',
+    'Framework :: Django :: 5.2',
+    'Topic :: Software Development :: Libraries :: Python Modules',
+]
+dependencies = ["django"]
+
+[dependency-groups]
+dev = [
+    "psycopg2-binary",
+    "pytest-unmagic",
+    "ruff",
+]
+
+[project.urls]
+Home = "https://github.com/dimagi/django-cte"
+
+[build-system]
+requires = ["flit_core >=3.2,<4"]
+build-backend = "flit_core.buildapi"
+
+[tool.flit.module]
+name = "django_cte"
+
+[tool.distutils.bdist_wheel]
+universal = true
diff -pruN 1.3.3-2/setup.cfg 2.0.0-0ubuntu1/setup.cfg
--- 1.3.3-2/setup.cfg	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/setup.cfg	1970-01-01 00:00:00.000000000 +0000
@@ -1,8 +0,0 @@
-[bdist_wheel]
-universal=1
-
-[metadata]
-license_file = LICENSE
-
-[flake8]
-exclude = ./build
diff -pruN 1.3.3-2/setup.py 2.0.0-0ubuntu1/setup.py
--- 1.3.3-2/setup.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/setup.py	1970-01-01 00:00:00.000000000 +0000
@@ -1,59 +0,0 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-import os
-import re
-from io import open
-
-from setuptools import find_packages, setup
-
-
-def get_version(filename):
-    path = os.path.join(os.path.dirname(__file__), filename)
-    with open(path, encoding="utf-8") as handle:
-        content = handle.read()
-    return re.search(r'__version__ = "([^"]+)"', content).group(1)
-
-
-def read_md(filename):
-    path = os.path.join(os.path.dirname(__file__), filename)
-    with open(path, encoding='utf-8') as handle:
-        return handle.read()
-
-
-setup(
-    name='django-cte',
-    version=get_version('django_cte/__init__.py'),
-    description='Common Table Expressions (CTE) for Django',
-    long_description=read_md('README.md'),
-    long_description_content_type='text/markdown',
-    maintainer='Daniel Miller',
-    maintainer_email='millerdev@gmail.com',
-    url='https://github.com/dimagi/django-cte',
-    license='BSD License',
-    packages=find_packages(exclude=['tests']),
-    include_package_data=True,
-    classifiers=[
-        'Development Status :: 3 - Alpha',
-        'Environment :: Web Environment',
-        'Framework :: Django',
-        'Intended Audience :: Developers',
-        'License :: OSI Approved :: BSD License',
-        'Operating System :: OS Independent',
-        'Programming Language :: Python',
-        'Programming Language :: Python :: 3',
-        'Programming Language :: Python :: 3.8',
-        'Programming Language :: Python :: 3.9',
-        'Programming Language :: Python :: 3.10',
-        'Programming Language :: Python :: 3.11',
-        'Programming Language :: Python :: 3.12',
-        'Framework :: Django',
-        'Framework :: Django :: 3',
-        'Framework :: Django :: 3.2',
-        'Framework :: Django :: 4',
-        'Framework :: Django :: 4.0',
-        'Framework :: Django :: 4.1',
-        'Framework :: Django :: 4.2',
-        'Topic :: Software Development :: Libraries :: Python Modules',
-    ],
-)
diff -pruN 1.3.3-2/tests/__init__.py 2.0.0-0ubuntu1/tests/__init__.py
--- 1.3.3-2/tests/__init__.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/__init__.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,9 +1,9 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 import os
+import warnings
+from contextlib import contextmanager
 
 import django
+from unmagic import fixture
 
 # django setup must occur before importing models
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
@@ -12,10 +12,23 @@ django.setup()
 from .django_setup import init_db, destroy_db  # noqa
 
 
-def setup():
-    """Initialize database for nosetests"""
-    init_db()
+@fixture(autouse=__file__, scope="package")
+def test_db():
+    with ignore_v1_warnings():
+        init_db()
+    yield
+    destroy_db()
 
 
-def teardown():
-    destroy_db()
+@contextmanager
+def ignore_v1_warnings():
+    msg = (
+        r"CTE(Manager|QuerySet) is deprecated.*"
+        r"|"
+        r"Use `django_cte\.with_cte\(.*\)` instead\."
+        r"|"
+        r"Use `django_cte\.CTE(\.recursive)?` instead\."
+    )
+    with warnings.catch_warnings():
+        warnings.filterwarnings("ignore", message=msg, category=DeprecationWarning)
+        yield
diff -pruN 1.3.3-2/tests/django_setup.py 2.0.0-0ubuntu1/tests/django_setup.py
--- 1.3.3-2/tests/django_setup.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/django_setup.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,8 +1,3 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-import sys
-
 from django.db import connection
 
 from .models import KeyPair, Region, Order
@@ -16,13 +11,7 @@ def init_db():
         return
     is_initialized = True
 
-    # replace sys.stdout for prompt to delete database
-    old_stdout = sys.stdout
-    sys.stdout = sys.__stdout__
-    try:
-        connection.creation.create_test_db(verbosity=0)
-    finally:
-        sys.stdout = old_stdout
+    connection.creation.create_test_db(verbosity=0, autoclobber=True)
 
     setup_data()
 
diff -pruN 1.3.3-2/tests/models.py 2.0.0-0ubuntu1/tests/models.py
--- 1.3.3-2/tests/models.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/models.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,9 +1,8 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 from django.db.models import (
     CASCADE,
+    Manager,
     Model,
+    QuerySet,
     AutoField,
     CharField,
     ForeignKey,
@@ -11,33 +10,20 @@ from django.db.models import (
     TextField,
 )
 
-from django_cte import CTEManager, CTEQuerySet
-
 
-class LT40QuerySet(CTEQuerySet):
+class LT40QuerySet(QuerySet):
 
     def lt40(self):
         return self.filter(amount__lt=40)
 
 
-class LT30QuerySet(CTEQuerySet):
-
-    def lt30(self):
-        return self.filter(amount__lt=30)
-
-
-class LT25QuerySet(CTEQuerySet):
+class LT25QuerySet(QuerySet):
 
     def lt25(self):
         return self.filter(amount__lt=25)
 
 
-class LTManager(CTEManager):
-    pass
-
-
 class Region(Model):
-    objects = CTEManager()
     name = TextField(primary_key=True)
     parent = ForeignKey("self", null=True, on_delete=CASCADE)
 
@@ -54,7 +40,6 @@ class User(Model):
 
 
 class Order(Model):
-    objects = CTEManager()
     id = AutoField(primary_key=True)
     region = ForeignKey(Region, on_delete=CASCADE)
     amount = IntegerField(default=0)
@@ -67,29 +52,16 @@ class Order(Model):
 class OrderFromLT40(Order):
     class Meta:
         proxy = True
-    objects = CTEManager.from_queryset(LT40QuerySet)()
-
-
-class OrderLT40AsManager(Order):
-    class Meta:
-        proxy = True
-    objects = LT40QuerySet.as_manager()
+    objects = Manager.from_queryset(LT40QuerySet)()
 
 
 class OrderCustomManagerNQuery(Order):
     class Meta:
         proxy = True
-    objects = LTManager.from_queryset(LT25QuerySet)()
-
-
-class OrderCustomManager(Order):
-    class Meta:
-        proxy = True
-    objects = LTManager()
+    objects = Manager.from_queryset(LT25QuerySet)()
 
 
 class KeyPair(Model):
-    objects = CTEManager()
     key = CharField(max_length=32)
     value = IntegerField(default=0)
     parent = ForeignKey("self", null=True, on_delete=CASCADE)
diff -pruN 1.3.3-2/tests/settings.py 2.0.0-0ubuntu1/tests/settings.py
--- 1.3.3-2/tests/settings.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/settings.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,6 +1,3 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
 import os
 import json
 
@@ -20,3 +17,4 @@ DATABASES = {'default': _db_settings}
 INSTALLED_APPS = ["tests"]
 
 SECRET_KEY = "test"
+USE_TZ = False
diff -pruN 1.3.3-2/tests/test_combinators.py 2.0.0-0ubuntu1/tests/test_combinators.py
--- 1.3.3-2/tests/test_combinators.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_combinators.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,270 @@
+import pytest
+from django.db.models import Value
+from django.db.models.aggregates import Sum
+from django.test import TestCase
+
+from django_cte import CTE, with_cte
+
+from .models import Order
+
+
+class TestCTECombinators(TestCase):
+
+    def test_cte_union_query(self):
+        one = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="one"
+        )
+        two = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount") * 2),
+            name="two"
+        )
+
+        earths = with_cte(
+            one,
+            select=one.join(
+                Order.objects.filter(region_id="earth"),
+                region=one.col.region_id
+            )
+            .annotate(region_total=one.col.total)
+            .values_list("amount", "region_id", "region_total")
+        )
+        mars = with_cte(
+            two,
+            select=two.join(
+                Order.objects.filter(region_id="mars"),
+                region=two.col.region_id
+            )
+            .annotate(region_total=two.col.total)
+            .values_list("amount", "region_id", "region_total")
+        )
+        combined = earths.union(mars, all=True)
+        print(combined.query)
+
+        self.assertEqual(sorted(combined), [
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+            (40, 'mars', 246),
+            (41, 'mars', 246),
+            (42, 'mars', 246),
+        ])
+
+        # queries used in union should still work on their own
+        print(earths.query)
+        self.assertEqual(sorted(earths),[
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+        ])
+        print(mars.query)
+        self.assertEqual(sorted(mars),[
+            (40, 'mars', 246),
+            (41, 'mars', 246),
+            (42, 'mars', 246),
+        ])
+
+    def test_cte_union_with_non_cte_query(self):
+        one = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+
+        earths = with_cte(
+            one,
+            select=one.join(
+                Order.objects.filter(region_id="earth"),
+                region=one.col.region_id
+            ).annotate(region_total=one.col.total)
+        )
+        plain_mars = (
+            Order.objects.filter(region_id="mars")
+            .annotate(region_total=Value(0))
+        )
+        # Note: this does not work in the opposite order. A CTE query
+        # must come first to invoke custom CTE combinator logic.
+        combined = earths.union(plain_mars, all=True) \
+            .values_list("amount", "region_id", "region_total")
+        print(combined.query)
+
+        self.assertEqual(sorted(combined), [
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+            (40, 'mars', 0),
+            (41, 'mars', 0),
+            (42, 'mars', 0),
+        ])
+
+    def test_cte_union_with_duplicate_names(self):
+        cte_sun = CTE(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+        cte_proxima = CTE(
+            Order.objects
+            .filter(region__parent="proxima centauri")
+            .values("region_id")
+            .annotate(total=2 * Sum("amount")),
+        )
+
+        orders_sun = with_cte(
+            cte_sun,
+            select=cte_sun.join(Order, region=cte_sun.col.region_id)
+            .annotate(region_total=cte_sun.col.total)
+        )
+        orders_proxima = with_cte(
+            cte_proxima,
+            select=cte_proxima.join(Order, region=cte_proxima.col.region_id)
+            .annotate(region_total=cte_proxima.col.total)
+        )
+
+        msg = "Found two or more CTEs named 'cte'"
+        with pytest.raises(ValueError, match=msg):
+            orders_sun.union(orders_proxima)
+
+    def test_cte_union_of_same_cte(self):
+        cte = CTE(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+
+        orders_big = with_cte(
+            cte,
+            select=cte.join(Order, region=cte.col.region_id)
+            .annotate(region_total=3 * cte.col.total)
+        )
+        orders_small = with_cte(
+            cte,
+            select=cte.join(Order, region=cte.col.region_id)
+            .annotate(region_total=cte.col.total)
+        )
+
+        orders = orders_big.union(orders_small) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+             (10, 'mercury', 33),
+             (10, 'mercury', 99),
+             (11, 'mercury', 33),
+             (11, 'mercury', 99),
+             (12, 'mercury', 33),
+             (12, 'mercury', 99),
+             (20, 'venus', 86),
+             (20, 'venus', 258),
+             (21, 'venus', 86),
+             (21, 'venus', 258),
+             (22, 'venus', 86),
+             (22, 'venus', 258),
+             (23, 'venus', 86),
+             (23, 'venus', 258),
+             (30, 'earth', 126),
+             (30, 'earth', 378),
+             (31, 'earth', 126),
+             (31, 'earth', 378),
+             (32, 'earth', 126),
+             (32, 'earth', 378),
+             (33, 'earth', 126),
+             (33, 'earth', 378),
+             (40, 'mars', 123),
+             (40, 'mars', 369),
+             (41, 'mars', 123),
+             (41, 'mars', 369),
+             (42, 'mars', 123),
+             (42, 'mars', 369)
+        ])
+
+    def test_cte_intersection(self):
+        cte_big = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='big'
+        )
+        cte_small = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='small'
+        )
+        orders_big = with_cte(
+            cte_big,
+            select=cte_big.join(Order, region=cte_big.col.region_id)
+            .annotate(region_total=cte_big.col.total)
+            .filter(region_total__gte=86)
+        )
+        orders_small = with_cte(
+            cte_small,
+            select=cte_small.join(Order, region=cte_small.col.region_id)
+            .annotate(region_total=cte_small.col.total)
+            .filter(region_total__lte=123)
+        )
+
+        orders = orders_small.intersection(orders_big) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+            (20, 'venus', 86),
+            (21, 'venus', 86),
+            (22, 'venus', 86),
+            (23, 'venus', 86),
+            (40, 'mars', 123),
+            (41, 'mars', 123),
+            (42, 'mars', 123),
+        ])
+
+    def test_cte_difference(self):
+        cte_big = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='big'
+        )
+        cte_small = CTE(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='small'
+        )
+        orders_big = with_cte(
+            cte_big,
+            select=cte_big.join(Order, region=cte_big.col.region_id)
+            .annotate(region_total=cte_big.col.total)
+            .filter(region_total__gte=86)
+        )
+        orders_small = with_cte(
+            cte_small,
+            select=cte_small.join(Order, region=cte_small.col.region_id)
+            .annotate(region_total=cte_small.col.total)
+            .filter(region_total__lte=123)
+        )
+
+        orders = orders_small.difference(orders_big) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+            (1, 'moon', 6),
+            (2, 'moon', 6),
+            (3, 'moon', 6),
+            (10, 'mercury', 33),
+            (10, 'proxima centauri b', 33),
+            (11, 'mercury', 33),
+            (11, 'proxima centauri b', 33),
+            (12, 'mercury', 33),
+            (12, 'proxima centauri b', 33),
+        ])
diff -pruN 1.3.3-2/tests/test_cte.py 2.0.0-0ubuntu1/tests/test_cte.py
--- 1.3.3-2/tests/test_cte.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_cte.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,19 +1,14 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-from __future__ import print_function
-from unittest import SkipTest
-
+import pytest
 from django.db.models import IntegerField, TextField
 from django.db.models.aggregates import Count, Max, Min, Sum
 from django.db.models.expressions import (
     Exists, ExpressionWrapper, F, OuterRef, Subquery,
 )
 from django.db.models.sql.constants import LOUTER
+from django.db.utils import OperationalError, ProgrammingError
 from django.test import TestCase
 
-from django_cte import With
-from django_cte import CTEManager
+from django_cte import CTE, with_cte
 
 from .models import Order, Region, User
 
@@ -24,22 +19,20 @@ text_field = TextField()
 class TestCTE(TestCase):
 
     def test_simple_cte_query(self):
-        cte = With(
+        cte = CTE(
             Order.objects
             .values("region_id")
             .annotate(total=Sum("amount"))
         )
 
-        orders = (
-            # FROM orders INNER JOIN cte ON orders.region_id = cte.region_id
-            cte.join(Order, region=cte.col.region_id)
-
-            # Add `WITH ...` before `SELECT ... FROM orders ...`
-            .with_cte(cte)
-
-            # Annotate each Order with a "region_total"
-            .annotate(region_total=cte.col.total)
-        )
+        orders = with_cte(
+            # WITH cte ...
+            cte,
+
+            # SELECT ... FROM orders
+            # INNER JOIN cte ON orders.region_id = cte.region_id
+            select=cte.join(Order, region=cte.col.region_id),
+        ).annotate(region_total=cte.col.total)
         print(orders.query)
 
         data = sorted((o.amount, o.region_id, o.region_total) for o in orders)
@@ -69,17 +62,16 @@ class TestCTE(TestCase):
         ])
 
     def test_cte_name_escape(self):
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(region__parent="sun")
             .values("region_id")
             .annotate(total=Sum("amount")),
             name="mixedCaseCTEName"
         )
-        orders = (
-            totals
-            .join(Order, region=totals.col.region_id)
-            .with_cte(totals)
+        orders = with_cte(
+            totals,
+            select=totals.join(Order, region=totals.col.region_id)
             .annotate(region_total=totals.col.total)
             .order_by("amount")
         )
@@ -87,15 +79,14 @@ class TestCTE(TestCase):
             str(orders.query).startswith('WITH RECURSIVE "mixedCaseCTEName"'))
 
     def test_cte_queryset(self):
-        sub_totals = With(
+        sub_totals = CTE(
             Order.objects
             .values(region_parent=F("region__parent_id"))
             .annotate(total=Sum("amount")),
         )
-        regions = (
-            Region.objects.all()
-            .with_cte(sub_totals)
-            .annotate(
+        regions = with_cte(
+            sub_totals,
+            select=Region.objects.annotate(
                 child_regions_total=Subquery(
                     sub_totals.queryset()
                     .filter(region_parent=OuterRef("name"))
@@ -122,11 +113,14 @@ class TestCTE(TestCase):
         ])
 
     def test_cte_queryset_with_model_result(self):
-        cte = With(
+        cte = CTE(
             Order.objects
             .annotate(region_parent=F("region__parent_id")),
         )
-        orders = cte.queryset().with_cte(cte)
+        orders = with_cte(
+            cte,         # WITH cte AS (...)
+            select=cte,  # SELECT ... FROM cte
+        )
         print(orders.query)
 
         data = sorted(
@@ -144,13 +138,13 @@ class TestCTE(TestCase):
         )
 
     def test_cte_queryset_with_join(self):
-        cte = With(
+        cte = CTE(
             Order.objects
             .annotate(region_parent=F("region__parent_id")),
         )
-        orders = (
-            cte.queryset()
-            .with_cte(cte)
+        orders = with_cte(
+            cte,
+            select=cte.queryset()
             .annotate(parent=F("region__parent_id"))
             .order_by("region_id", "amount")
         )
@@ -166,7 +160,7 @@ class TestCTE(TestCase):
         ])
 
     def test_cte_queryset_with_values_result(self):
-        cte = With(
+        cte = CTE(
             Order.objects
             .values(
                 "region_id",
@@ -174,11 +168,7 @@ class TestCTE(TestCase):
             )
             .distinct()
         )
-        values = (
-            cte.queryset()
-            .with_cte(cte)
-            .filter(region_parent__isnull=False)
-        )
+        values = with_cte(cte, select=cte).filter(region_parent__isnull=False)
         print(values.query)
 
         def key(item):
@@ -197,27 +187,27 @@ class TestCTE(TestCase):
         ])
 
     def test_named_simple_ctes(self):
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(region__parent="sun")
             .values("region_id")
             .annotate(total=Sum("amount")),
             name="totals",
         )
-        region_count = With(
+        region_count = CTE(
             Region.objects
             .filter(parent="sun")
-            .values("parent")
+            .values("parent_id")
             .annotate(num=Count("name")),
             name="region_count",
         )
-        orders = (
-            region_count.join(
+        orders = with_cte(
+            totals,
+            region_count,
+            select=region_count.join(
                 totals.join(Order, region=totals.col.region_id),
                 region__parent=region_count.col.parent_id
             )
-            .with_cte(totals)
-            .with_cte(region_count)
             .annotate(region_total=totals.col.total)
             .annotate(region_count=region_count.col.num)
             .order_by("amount")
@@ -261,9 +251,9 @@ class TestCTE(TestCase):
                 ),
                 all=True,
             )
-        rootmap = With.recursive(make_root_mapping, name="rootmap")
+        rootmap = CTE.recursive(make_root_mapping, name="rootmap")
 
-        totals = With(
+        totals = CTE(
             rootmap.join(Order, region_id=rootmap.col.name)
             .values(
                 root=rootmap.col.root,
@@ -274,11 +264,10 @@ class TestCTE(TestCase):
             name="totals",
         )
 
-        root_regions = (
-            totals.join(Region, name=totals.col.root)
-            .with_cte(rootmap)
-            .with_cte(totals)
-            .annotate(
+        root_regions = with_cte(
+            rootmap,
+            totals,
+            select=totals.join(Region, name=totals.col.root).annotate(
                 # count of orders in this region and all subregions
                 orders_count=totals.col.orders_count,
                 # sum of order amounts in this region and all subregions
@@ -296,17 +285,16 @@ class TestCTE(TestCase):
         ])
 
     def test_materialized_option(self):
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(region__parent="sun")
             .values("region_id")
             .annotate(total=Sum("amount")),
             materialized=True
         )
-        orders = (
-            totals
-            .join(Order, region=totals.col.region_id)
-            .with_cte(totals)
+        orders = with_cte(
+            totals,
+            select=totals.join(Order, region=totals.col.region_id)
             .annotate(region_total=totals.col.total)
             .order_by("amount")
         )
@@ -317,14 +305,14 @@ class TestCTE(TestCase):
         )
 
     def test_update_cte_query(self):
-        cte = With(
+        cte = CTE(
             Order.objects
             .values(region_parent=F("region__parent_id"))
             .annotate(total=Sum("amount"))
             .filter(total__isnull=False)
         )
         # not the most efficient query, but it exercises CTEUpdateQuery
-        Order.objects.all().with_cte(cte).filter(region_id__in=Subquery(
+        with_cte(cte, select=Order).filter(region_id__in=Subquery(
             cte.queryset()
             .filter(region_parent=OuterRef("region_id"))
             .values("region_parent")
@@ -348,7 +336,7 @@ class TestCTE(TestCase):
 
     def test_update_with_subquery(self):
         # Test for issue: https://github.com/dimagi/django-cte/issues/9
-        # Issue is not reproduces on sqlite3 use postgres to run.
+        # Issue is not reproduced on sqlite3, use postgres to run.
         # To reproduce the problem it's required to have some join
         # in the select-query so the compiler will turn it into a subquery.
         # To add a join use a filter over field of related model
@@ -362,19 +350,21 @@ class TestCTE(TestCase):
             ('mars', 0),
         })
 
+    @pytest.mark.xfail(
+        reason="this test will not work until `QuerySet.delete` "
+            "(Django method) calls `self.query.chain(sql.DeleteQuery)` "
+            "instead of `sql.DeleteQuery(self.model)`",
+        raises=(OperationalError, ProgrammingError),
+        strict=True,
+    )
     def test_delete_cte_query(self):
-        raise SkipTest(
-            "this test will not work until `QuerySet.delete` (Django method) "
-            "calls `self.query.chain(sql.DeleteQuery)` instead of "
-            "`sql.DeleteQuery(self.model)`"
-        )
-        cte = With(
+        cte = CTE(
             Order.objects
             .values(region_parent=F("region__parent_id"))
             .annotate(total=Sum("amount"))
             .filter(total__isnull=False)
         )
-        Order.objects.all().with_cte(cte).annotate(
+        with_cte(cte, select=Order).annotate(
             cte_has_order=Exists(
                 cte.queryset()
                 .values("total")
@@ -395,7 +385,7 @@ class TestCTE(TestCase):
     def test_outerref_in_cte_query(self):
         # This query is meant to return the difference between min and max
         # order of each region, through a subquery
-        min_and_max = With(
+        min_and_max = CTE(
             Order.objects
             .filter(region=OuterRef("pk"))
             .values('region')  # This is to force group by region_id
@@ -409,7 +399,8 @@ class TestCTE(TestCase):
             Region.objects
             .annotate(
                 difference=Subquery(
-                    min_and_max.queryset().with_cte(min_and_max).annotate(
+                    with_cte(min_and_max, select=min_and_max)
+                    .annotate(
                         difference=ExpressionWrapper(
                             F('amount_max') - F('amount_min'),
                             output_field=int_field,
@@ -438,16 +429,16 @@ class TestCTE(TestCase):
         ])
 
     def test_experimental_left_outer_join(self):
-        totals = With(
+        totals = CTE(
             Order.objects
             .values("region_id")
             .annotate(total=Sum("amount"))
             .filter(total__gt=100)
         )
-        orders = (
-            totals
+        orders = with_cte(
+            totals,
+            select=totals
             .join(Order, region=totals.col.region_id, _join_type=LOUTER)
-            .with_cte(totals)
             .annotate(region_total=totals.col.total)
         )
         print(orders.query)
@@ -486,9 +477,7 @@ class TestCTE(TestCase):
         subquery model doesn't use the CTE manager, and the query results
         match expected behavior
         """
-        self.assertNotIsInstance(User.objects, CTEManager)
-
-        sub_totals = With(
+        sub_totals = CTE(
             Order.objects
             .values(region_parent=F("region__parent_id"))
             .annotate(
@@ -500,10 +489,9 @@ class TestCTE(TestCase):
                 ),
             ),
         )
-        regions = (
-            Region.objects.all()
-            .with_cte(sub_totals)
-            .annotate(
+        regions = with_cte(
+            sub_totals,
+            select=Region.objects.annotate(
                 child_regions_total=Subquery(
                     sub_totals.queryset()
                     .filter(region_parent=OuterRef("name"))
@@ -535,31 +523,32 @@ class TestCTE(TestCase):
         correct position
         """
 
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(region__parent="sun")
             .values("region_id")
             .annotate(total=Sum("amount")),
             name="totals",
         )
-        region_count = With(
+        region_count = CTE(
             Region.objects
             .filter(parent="sun")
-            .values("parent")
+            .values("parent_id")
             .annotate(num=Count("name")),
             name="region_count",
         )
-        orders = (
-            region_count.join(
+        orders = with_cte(
+            totals,
+            region_count,
+            select=region_count.join(
                 totals.join(Order, region=totals.col.region_id),
                 region__parent=region_count.col.parent_id
             )
-            .with_cte(totals)
-            .with_cte(region_count)
             .annotate(region_total=totals.col.total)
             .annotate(region_count=region_count.col.num)
             .order_by("amount")
         )
+        print(orders.query)
 
         self.assertIsInstance(orders.explain(), str)
 
@@ -568,16 +557,16 @@ class TestCTE(TestCase):
         Verifies that the CTEQueryCompiler can handle empty result sets in the
         related CTEs
         """
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(id__in=[])
             .values("region_id")
             .annotate(total=Sum("amount")),
             name="totals",
         )
-        orders = (
-            totals.join(Order, region=totals.col.region_id)
-            .with_cte(totals)
+        orders = with_cte(
+            totals,
+            select=totals.join(Order, region=totals.col.region_id)
             .annotate(region_total=totals.col.total)
             .order_by("amount")
         )
@@ -585,18 +574,97 @@ class TestCTE(TestCase):
         self.assertEqual(len(orders), 0)
 
     def test_left_outer_join_on_empty_result_set_cte(self):
-        totals = With(
+        totals = CTE(
             Order.objects
             .filter(id__in=[])
             .values("region_id")
             .annotate(total=Sum("amount")),
             name="totals",
         )
-        orders = (
-            totals.join(Order, region=totals.col.region_id, _join_type=LOUTER)
-            .with_cte(totals)
+        orders = with_cte(
+            totals,
+            select=totals
+            .join(Order, region=totals.col.region_id, _join_type=LOUTER)
             .annotate(region_total=totals.col.total)
             .order_by("amount")
         )
 
         self.assertEqual(len(orders), 22)
+
+    def test_union_query_with_cte(self):
+        orders = (
+            Order.objects
+            .filter(region__parent="sun")
+            .only("region", "amount")
+        )
+        orders_cte = CTE(orders, name="orders_cte")
+        orders_cte_queryset = orders_cte.queryset()
+
+        earth_orders = orders_cte_queryset.filter(region="earth")
+        mars_orders = orders_cte_queryset.filter(region="mars")
+
+        earth_mars = earth_orders.union(mars_orders, all=True)
+        earth_mars_cte = with_cte(
+            orders_cte,
+            select=earth_mars
+            .order_by("region", "amount")
+            .values_list("region", "amount")
+        )
+        print(earth_mars_cte.query)
+
+        self.assertEqual(list(earth_mars_cte), [
+            ('earth', 30),
+            ('earth', 31),
+            ('earth', 32),
+            ('earth', 33),
+            ('mars', 40),
+            ('mars', 41),
+            ('mars', 42),
+        ])
+
+    def test_cte_select_pk(self):
+        orders = Order.objects.filter(region="earth").values("pk")
+        cte = CTE(orders)
+        queryset = with_cte(
+            cte, select=cte.join(orders, pk=cte.col.pk)
+        ).order_by("pk")
+        print(queryset.query)
+        self.assertEqual(list(queryset), [
+            {'pk': 9},
+            {'pk': 10},
+            {'pk': 11},
+            {'pk': 12},
+        ])
+
+    def test_django52_resolve_ref_regression(self):
+        cte = CTE(
+            Order.objects.annotate(
+                pnt_id=F("region__parent_id"),
+                region_name=F("region__name"),
+            ).values(
+                # important: more than one query.select field
+                "region_id",
+                "amount",
+                # important: more than one query.annotations field
+                "pnt_id",
+                "region_name",
+            )
+        )
+        qs = with_cte(
+            cte,
+            select=cte.queryset()
+            .values(
+                amt=cte.col.amount,
+                pnt_id=cte.col.pnt_id,
+                region_name=cte.col.region_name,
+            )
+            .filter(region_id="earth")
+            .order_by("amount")
+        )
+        print(qs.query)
+        self.assertEqual(list(qs), [
+            {'amt': 30, 'region_name': 'earth', 'pnt_id': 'sun'},
+            {'amt': 31, 'region_name': 'earth', 'pnt_id': 'sun'},
+            {'amt': 32, 'region_name': 'earth', 'pnt_id': 'sun'},
+            {'amt': 33, 'region_name': 'earth', 'pnt_id': 'sun'},
+        ])
diff -pruN 1.3.3-2/tests/test_django.py 2.0.0-0ubuntu1/tests/test_django.py
--- 1.3.3-2/tests/test_django.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_django.py	2025-06-16 15:26:31.000000000 +0000
@@ -4,52 +4,11 @@ import django
 from django.db import OperationalError, ProgrammingError
 from django.db.models import Window
 from django.db.models.functions import Rank
-from django.test import TestCase, skipUnlessDBFeature
+from django.test import TestCase
 
-from .models import Order, Region, User
+from django_cte import CTE, with_cte
 
-
-@skipUnlessDBFeature("supports_select_union")
-class NonCteQueries(TestCase):
-    """Test non-CTE queries
-
-    These tests were adapted from the Django test suite. The models used
-    here use CTEManager and CTEQuerySet to verify feature parity with
-    their base classes Manager and QuerySet.
-    """
-
-    @classmethod
-    def setUpTestData(cls):
-        Order.objects.all().delete()
-
-    def test_union_with_select_related_and_order(self):
-        e1 = User.objects.create(name="e1")
-        a1 = Order.objects.create(region_id="earth", user=e1)
-        a2 = Order.objects.create(region_id="moon", user=e1)
-        Order.objects.create(region_id="sun", user=e1)
-        base_qs = Order.objects.select_related("user").order_by()
-        qs1 = base_qs.filter(region_id="earth")
-        qs2 = base_qs.filter(region_id="moon")
-        print(qs1.union(qs2).order_by("pk").query)
-        self.assertSequenceEqual(qs1.union(qs2).order_by("pk"), [a1, a2])
-
-    @skipUnlessDBFeature("supports_slicing_ordering_in_compound")
-    def test_union_with_select_related_and_first(self):
-        e1 = User.objects.create(name="e1")
-        a1 = Order.objects.create(region_id="earth", user=e1)
-        Order.objects.create(region_id="moon", user=e1)
-        base_qs = Order.objects.select_related("user")
-        qs1 = base_qs.filter(region_id="earth")
-        qs2 = base_qs.filter(region_id="moon")
-        self.assertEqual(qs1.union(qs2).first(), a1)
-
-    def test_union_with_first(self):
-        e1 = User.objects.create(name="e1")
-        a1 = Order.objects.create(region_id="earth", user=e1)
-        base_qs = Order.objects.order_by()
-        qs1 = base_qs.filter(region_id="earth")
-        qs2 = base_qs.filter(region_id="moon")
-        self.assertEqual(qs1.union(qs2).first(), a1)
+from .models import Order, Region
 
 
 class WindowFunctions(TestCase):
@@ -57,8 +16,7 @@ class WindowFunctions(TestCase):
     def test_heterogeneous_filter_in_cte(self):
         if django.VERSION < (4, 2):
             raise SkipTest("feature added in Django 4.2")
-        from django_cte import With
-        cte = With(
+        cte = CTE(
             Order.objects.annotate(
                 region_amount_rank=Window(
                     Rank(), partition_by="region_id", order_by="-amount"
@@ -68,14 +26,14 @@ class WindowFunctions(TestCase):
             .values("region_id", "region_amount_rank")
             .filter(region_amount_rank=1, region_id__in=["sun", "moon"])
         )
-        qs = cte.join(Region, name=cte.col.region_id).with_cte(cte)
+        qs = with_cte(cte, select=cte.join(Region, name=cte.col.region_id))
         print(qs.query)
         # ProgrammingError: column cte.region_id does not exist
         # WITH RECURSIVE "cte" AS (SELECT * FROM (
         #   SELECT "orders"."region_id" AS "col1", ...
         # "region" INNER JOIN "cte" ON "region"."name" = ("cte"."region_id")
         try:
-            self.assertSequenceEqual({r.name for r in qs}, {"moon", "sun"})
+            self.assertEqual({r.name for r in qs}, {"moon", "sun"})
         except (OperationalError, ProgrammingError) as err:
             if "cte.region_id" in str(err):
                 raise SkipTest(
@@ -83,4 +41,5 @@ class WindowFunctions(TestCase):
                     "column references"
                 )
             raise
-        assert 0, "unexpected pass"
+        if django.VERSION < (5, 2):
+            assert 0, "unexpected pass"
diff -pruN 1.3.3-2/tests/test_manager.py 2.0.0-0ubuntu1/tests/test_manager.py
--- 1.3.3-2/tests/test_manager.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_manager.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,65 +1,28 @@
-from __future__ import absolute_import
-from __future__ import unicode_literals
-
-from __future__ import print_function
 from django.db.models.expressions import F
-from django.db.models.query import QuerySet
 from django.test import TestCase
 
-from django_cte import With, CTEQuerySet, CTEManager
+from django_cte import CTE, with_cte
 
 from .models import (
-    Order,
     OrderFromLT40,
-    OrderLT40AsManager,
     OrderCustomManagerNQuery,
-    OrderCustomManager,
     LT40QuerySet,
-    LTManager,
-    LT25QuerySet,
 )
 
 
 class TestCTE(TestCase):
-    def test_cte_queryset_correct_defaultmanager(self):
-        self.assertEqual(type(Order._default_manager), CTEManager)
-        self.assertEqual(type(Order.objects.all()), CTEQuerySet)
-
-    def test_cte_queryset_correct_from_queryset(self):
-        self.assertEqual(type(OrderFromLT40.objects.all()), LT40QuerySet)
-
-    def test_cte_queryset_correct_queryset_as_manager(self):
-        self.assertEqual(type(OrderLT40AsManager.objects.all()), LT40QuerySet)
-
-    def test_cte_queryset_correct_manager_n_from_queryset(self):
-        self.assertIsInstance(
-            OrderCustomManagerNQuery._default_manager, LTManager)
-        self.assertEqual(type(
-            OrderCustomManagerNQuery.objects.all()), LT25QuerySet)
-
-    def test_cte_create_manager_from_non_cteQuery(self):
-        class BrokenQuerySet(QuerySet):
-            "This should be a CTEQuerySet if we want this to work"
-
-        with self.assertRaises(TypeError):
-            CTEManager.from_queryset(BrokenQuerySet)()
-
-    def test_cte_queryset_correct_limitedmanager(self):
-        self.assertEqual(type(OrderCustomManager._default_manager), LTManager)
-        # Check the expected even if not ideal behavior occurs
-        self.assertIsInstance(OrderCustomManager.objects.all(), CTEQuerySet)
 
     def test_cte_queryset_with_from_queryset(self):
         self.assertEqual(type(OrderFromLT40.objects.all()), LT40QuerySet)
 
-        cte = With(
+        cte = CTE(
             OrderFromLT40.objects
             .annotate(region_parent=F("region__parent_id"))
             .filter(region__parent_id="sun")
         )
-        orders = (
-            cte.queryset()
-            .with_cte(cte)
+        orders = with_cte(
+            cte,
+            select=cte.queryset()
             .lt40()  # custom queryset method
             .order_by("region_id", "amount")
         )
@@ -81,14 +44,14 @@ class TestCTE(TestCase):
         ])
 
     def test_cte_queryset_with_custom_queryset(self):
-        cte = With(
+        cte = CTE(
             OrderCustomManagerNQuery.objects
             .annotate(region_parent=F("region__parent_id"))
             .filter(region__parent_id="sun")
         )
-        orders = (
-            cte.queryset()
-            .with_cte(cte)
+        orders = with_cte(
+            cte,
+            select=cte.queryset()
             .lt25()  # custom queryset method
             .order_by("region_id", "amount")
         )
@@ -106,10 +69,10 @@ class TestCTE(TestCase):
         ])
 
     def test_cte_queryset_with_deferred_loading(self):
-        cte = With(
+        cte = CTE(
             OrderCustomManagerNQuery.objects.order_by("id").only("id")[:1]
         )
-        orders = cte.queryset().with_cte(cte)
+        orders = with_cte(cte, select=cte)
         print(orders.query)
 
         self.assertEqual([x.id for x in orders], [1])
diff -pruN 1.3.3-2/tests/test_raw.py 2.0.0-0ubuntu1/tests/test_raw.py
--- 1.3.3-2/tests/test_raw.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_raw.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,11 +1,7 @@
-from __future__ import absolute_import
-from __future__ import print_function
-from __future__ import unicode_literals
-
 from django.db.models import IntegerField, TextField
 from django.test import TestCase
 
-from django_cte import With
+from django_cte import CTE, with_cte
 from django_cte.raw import raw_cte_sql
 
 from .models import Region
@@ -17,7 +13,7 @@ text_field = TextField()
 class TestRawCTE(TestCase):
 
     def test_raw_cte_sql(self):
-        cte = With(raw_cte_sql(
+        cte = CTE(raw_cte_sql(
             """
             SELECT region_id, AVG(amount) AS avg_order
             FROM orders
@@ -27,19 +23,16 @@ class TestRawCTE(TestCase):
             ["moon"],
             {"region_id": text_field, "avg_order": int_field},
         ))
-        moon_avg = (
-            cte
-            .join(Region, name=cte.col.region_id)
-            .annotate(avg_order=cte.col.avg_order)
-            .with_cte(cte)
-        )
+        moon_avg = with_cte(
+            cte, select=cte.join(Region, name=cte.col.region_id)
+        ).annotate(avg_order=cte.col.avg_order)
         print(moon_avg.query)
 
         data = [(r.name, r.parent.name, r.avg_order) for r in moon_avg]
         self.assertEqual(data, [('moon', 'earth', 2)])
 
     def test_raw_cte_sql_name_escape(self):
-        cte = With(
+        cte = CTE(
             raw_cte_sql(
                 """
                 SELECT region_id, AVG(amount) AS avg_order
@@ -52,12 +45,9 @@ class TestRawCTE(TestCase):
             ),
             name="mixedCaseCTEName"
         )
-        moon_avg = (
-            cte
-            .join(Region, name=cte.col.region_id)
-            .annotate(avg_order=cte.col.avg_order)
-            .with_cte(cte)
-        )
+        moon_avg = with_cte(
+            cte, select=cte.join(Region, name=cte.col.region_id)
+        ).annotate(avg_order=cte.col.avg_order)
         self.assertTrue(
             str(moon_avg.query).startswith(
                 'WITH RECURSIVE "mixedCaseCTEName"')
diff -pruN 1.3.3-2/tests/test_recursive.py 2.0.0-0ubuntu1/tests/test_recursive.py
--- 1.3.3-2/tests/test_recursive.py	2024-06-07 12:37:52.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_recursive.py	2025-06-16 15:26:31.000000000 +0000
@@ -1,7 +1,3 @@
-from __future__ import absolute_import
-from __future__ import print_function
-from __future__ import unicode_literals
-
 import pickle
 from unittest import SkipTest
 
@@ -20,7 +16,7 @@ from django.db.models.functions import C
 from django.db.utils import DatabaseError
 from django.test import TestCase
 
-from django_cte import With
+from django_cte import CTE, with_cte
 
 from .models import KeyPair, Region
 
@@ -52,11 +48,11 @@ class TestRecursiveCTE(TestCase):
                 all=True,
             )
 
-        cte = With.recursive(make_regions_cte)
+        cte = CTE.recursive(make_regions_cte)
 
-        regions = (
-            cte.join(Region, name=cte.col.name)
-            .with_cte(cte)
+        regions = with_cte(
+            cte,
+            select=cte.join(Region, name=cte.col.name)
             .annotate(
                 path=cte.col.path,
                 depth=cte.col.depth,
@@ -112,8 +108,10 @@ class TestRecursiveCTE(TestCase):
                 ),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        regions = cte.join(Region, name=cte.col.name).with_cte(cte).annotate(
+        cte = CTE.recursive(make_regions_cte)
+        regions = with_cte(
+            cte, select=cte.join(Region, name=cte.col.name)
+        ).annotate(
             path=cte.col.path,
             depth=cte.col.depth,
             is_planet=cte.col.is_planet,
@@ -139,8 +137,8 @@ class TestRecursiveCTE(TestCase):
                 cte.join(Region, parent=cte.col.name),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        regions = cte.join(Region, name=cte.col.name).with_cte(cte)
+        cte = CTE.recursive(make_regions_cte)
+        regions = with_cte(cte, select=cte.join(Region, name=cte.col.name))
 
         print(regions.query)
         try:
@@ -168,8 +166,8 @@ class TestRecursiveCTE(TestCase):
             return cte.join(Region, parent=cte.col.name).values(
                 depth=cte.col.depth + 1,
             )
-        cte = With.recursive(make_bad_cte)
-        regions = cte.join(Region, name=cte.col.name).with_cte(cte)
+        cte = CTE.recursive(make_bad_cte)
+        regions = with_cte(cte, select=cte.join(Region, name=cte.col.name))
         with self.assertRaises(ValueError) as context:
             print(regions.query)
         self.assertIn("Circular reference:", str(context.exception))
@@ -188,11 +186,10 @@ class TestRecursiveCTE(TestCase):
                 ),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        regions = (
-            Region.objects.all()
-            .with_cte(cte)
-            .annotate(_ex=Exists(
+        cte = CTE.recursive(make_regions_cte)
+        regions = with_cte(
+            cte,
+            select=Region.objects.annotate(_ex=Exists(
                 cte.queryset()
                 .values(value=Value("1", output_field=int_field))
                 .filter(name=OuterRef("name"))
@@ -217,8 +214,8 @@ class TestRecursiveCTE(TestCase):
                 ),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        regions = cte.queryset().with_cte(cte).filter(depth=2).order_by("name")
+        cte = CTE.recursive(make_regions_cte)
+        regions = with_cte(cte, select=cte).filter(depth=2).order_by("name")
 
         pickled_qs = pickle.loads(pickle.dumps(regions))
 
@@ -234,30 +231,28 @@ class TestRecursiveCTE(TestCase):
                 value=F('name'),
             ).union(
                 cte.join(
-                    Region.objects.all().annotate(
-                        value=F('name'),
-                    ),
+                    Region.objects.annotate(value=F('name')),
                     parent_id=cte.col.name,
                 ),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        query = cte.queryset().with_cte(cte)
+        cte = CTE.recursive(make_regions_cte)
+        query = with_cte(cte, select=cte)
 
-        exclude_leaves = With(cte.queryset().filter(
+        exclude_leaves = CTE(cte.queryset().filter(
             parent__name='sun',
         ).annotate(
             value=Concat(F('name'), F('name'))
         ), name='value_cte')
 
-        query = query.annotate(
+        query = with_cte(exclude_leaves, select=query.annotate(
             _exclude_leaves=Exists(
                 exclude_leaves.queryset().filter(
                     name=OuterRef("name"),
                     value=OuterRef("value"),
                 )
             )
-        ).filter(_exclude_leaves=True).with_cte(exclude_leaves)
+        ).filter(_exclude_leaves=True))
         print(query.query)
 
         # Nothing should be returned.
@@ -272,23 +267,23 @@ class TestRecursiveCTE(TestCase):
                 rank=F('value'),
             ).union(
                 cte.join(
-                    KeyPair.objects.all().order_by(),
+                    KeyPair.objects.order_by(),
                     parent_id=cte.col.id,
                 ).annotate(
                     rank=F('value'),
                 ),
                 all=True,
             )
-        cte = With.recursive(make_regions_cte)
-        children = cte.queryset().with_cte(cte)
+        cte = CTE.recursive(make_regions_cte)
+        children = with_cte(cte, select=cte)
 
-        xdups = With(cte.queryset().filter(
+        xdups = CTE(cte.queryset().filter(
             parent__key="level 1",
         ).annotate(
             rank=F('value')
         ).values('id', 'rank'), name='xdups')
 
-        children = children.annotate(
+        children = with_cte(xdups, select=children.annotate(
             _exclude=Exists(
                 (
                     xdups.queryset().filter(
@@ -297,7 +292,7 @@ class TestRecursiveCTE(TestCase):
                     )
                 )
             )
-        ).filter(_exclude=True).with_cte(xdups)
+        ).filter(_exclude=True))
 
         print(children.query)
         query = KeyPair.objects.filter(parent__in=children)
@@ -315,10 +310,25 @@ class TestRecursiveCTE(TestCase):
         # This test covers MATERIALIZED option in SQL query
         def make_regions_cte(cte):
             return KeyPair.objects.all()
-        cte = With.recursive(make_regions_cte, materialized=True)
+        cte = CTE.recursive(make_regions_cte, materialized=True)
 
-        query = KeyPair.objects.with_cte(cte)
+        query = with_cte(cte, select=KeyPair)
         print(query.query)
         self.assertTrue(
             str(query.query).startswith('WITH RECURSIVE "cte" AS MATERIALIZED')
         )
+
+    def test_recursive_self_queryset(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                pk="earth"
+            ).values("pk").union(
+                cte.join(Region, parent=cte.col.pk).values("pk")
+            )
+        cte = CTE.recursive(make_regions_cte)
+        queryset = with_cte(cte, select=cte).order_by("pk")
+        print(queryset.query)
+        self.assertEqual(list(queryset), [
+            {'pk': 'earth'},
+            {'pk': 'moon'},
+        ])
diff -pruN 1.3.3-2/tests/test_v1/__init__.py 2.0.0-0ubuntu1/tests/test_v1/__init__.py
--- 1.3.3-2/tests/test_v1/__init__.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/__init__.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,19 @@
+from unmagic import fixture
+
+from .. import ignore_v1_warnings
+
+
+@fixture(autouse=__file__)
+def ignore_v1_deprecations():
+    with ignore_v1_warnings():
+        yield
+
+
+@fixture(autouse=__file__, scope="class")
+def ignore_v1_deprecations_in_class_setup():
+    with ignore_v1_warnings():
+        yield
+
+
+with ignore_v1_warnings():
+    from . import models  # noqa: F401
diff -pruN 1.3.3-2/tests/test_v1/models.py 2.0.0-0ubuntu1/tests/test_v1/models.py
--- 1.3.3-2/tests/test_v1/models.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/models.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,97 @@
+from django.db.models import Manager
+
+from django_cte import CTEManager, CTEQuerySet
+
+from ..models import (
+    KeyPair as V2KeyPair,
+    Order as V2Order,
+    Region as V2Region,
+    User,  # noqa: F401
+)
+
+
+class LT40QuerySet(CTEQuerySet):
+
+    def lt40(self):
+        return self.filter(amount__lt=40)
+
+
+class LT30QuerySet(CTEQuerySet):
+
+    def lt30(self):
+        return self.filter(amount__lt=30)
+
+
+class LT25QuerySet(CTEQuerySet):
+
+    def lt25(self):
+        return self.filter(amount__lt=25)
+
+
+class LTManager(CTEManager):
+    pass
+
+
+class V1Region(V2Region):
+    objects = CTEManager()
+
+    class Meta:
+        proxy = True
+
+
+Region = V1Region
+
+
+class V1Order(V2Order):
+    objects = CTEManager()
+
+    class Meta:
+        proxy = True
+
+
+Order = V1Order
+
+
+class V1OrderFromLT40(Order):
+    class Meta:
+        proxy = True
+    objects = CTEManager.from_queryset(LT40QuerySet)()
+
+
+class V1OrderLT40AsManager(Order):
+    class Meta:
+        proxy = True
+    objects = LT40QuerySet.as_manager()
+
+
+class V1OrderCustomManagerNQuery(Order):
+    class Meta:
+        proxy = True
+    objects = LTManager.from_queryset(LT25QuerySet)()
+
+
+class V1OrderCustomManager(Order):
+    class Meta:
+        proxy = True
+    objects = LTManager()
+
+
+class V1OrderPlainManager(Order):
+    class Meta:
+        proxy = True
+    objects = Manager()
+
+
+class V1KeyPair(V2KeyPair):
+    objects = CTEManager()
+
+    class Meta:
+        proxy = True
+
+
+KeyPair = V1KeyPair
+OrderCustomManager = V1OrderCustomManager
+OrderCustomManagerNQuery = V1OrderCustomManagerNQuery
+OrderFromLT40 = V1OrderFromLT40
+OrderLT40AsManager = V1OrderLT40AsManager
+OrderPlainManager = V1OrderPlainManager
diff -pruN 1.3.3-2/tests/test_v1/test_combinators.py 2.0.0-0ubuntu1/tests/test_v1/test_combinators.py
--- 1.3.3-2/tests/test_v1/test_combinators.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_combinators.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,271 @@
+import pytest
+from django.db.models import Value
+from django.db.models.aggregates import Sum
+from django.test import TestCase
+
+from django_cte import With
+
+from .models import Order, OrderPlainManager
+
+
+class TestCTECombinators(TestCase):
+
+    def test_cte_union_query(self):
+        one = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="one"
+        )
+        two = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount") * 2),
+            name="two"
+        )
+
+        earths = (
+            one.join(
+                Order.objects.filter(region_id="earth"),
+                region=one.col.region_id
+            )
+            .with_cte(one)
+            .annotate(region_total=one.col.total)
+            .values_list("amount", "region_id", "region_total")
+        )
+        mars = (
+            two.join(
+                Order.objects.filter(region_id="mars"),
+                region=two.col.region_id
+            )
+            .with_cte(two)
+            .annotate(region_total=two.col.total)
+            .values_list("amount", "region_id", "region_total")
+        )
+        combined = earths.union(mars, all=True)
+        print(combined.query)
+
+        self.assertEqual(sorted(combined), [
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+            (40, 'mars', 246),
+            (41, 'mars', 246),
+            (42, 'mars', 246),
+        ])
+
+        # queries used in union should still work on their own
+        print(earths.query)
+        self.assertEqual(sorted(earths),[
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+        ])
+        print(mars.query)
+        self.assertEqual(sorted(mars),[
+            (40, 'mars', 246),
+            (41, 'mars', 246),
+            (42, 'mars', 246),
+        ])
+
+    def test_cte_union_with_non_cte_query(self):
+        one = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+
+        earths = (
+            one.join(
+                Order.objects.filter(region_id="earth"),
+                region=one.col.region_id
+            )
+            .with_cte(one)
+            .annotate(region_total=one.col.total)
+        )
+        plain_mars = (
+            OrderPlainManager.objects.filter(region_id="mars")
+            .annotate(region_total=Value(0))
+        )
+        # Note: this does not work in the opposite order. A CTE query
+        # must come first to invoke custom CTE combinator logic.
+        combined = earths.union(plain_mars, all=True) \
+            .values_list("amount", "region_id", "region_total")
+        print(combined.query)
+
+        self.assertEqual(sorted(combined), [
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+            (40, 'mars', 0),
+            (41, 'mars', 0),
+            (42, 'mars', 0),
+        ])
+
+    def test_cte_union_with_duplicate_names(self):
+        cte_sun = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+        cte_proxima = With(
+            Order.objects
+            .filter(region__parent="proxima centauri")
+            .values("region_id")
+            .annotate(total=2 * Sum("amount")),
+        )
+
+        orders_sun = (
+            cte_sun.join(Order, region=cte_sun.col.region_id)
+            .with_cte(cte_sun)
+            .annotate(region_total=cte_sun.col.total)
+        )
+        orders_proxima = (
+            cte_proxima.join(Order, region=cte_proxima.col.region_id)
+            .with_cte(cte_proxima)
+            .annotate(region_total=cte_proxima.col.total)
+        )
+
+        msg = "Found two or more CTEs named 'cte'"
+        with pytest.raises(ValueError, match=msg):
+            orders_sun.union(orders_proxima)
+
+    def test_cte_union_of_same_cte(self):
+        cte = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+        )
+
+        orders_big = (
+            cte.join(Order, region=cte.col.region_id)
+            .with_cte(cte)
+            .annotate(region_total=3 * cte.col.total)
+        )
+        orders_small = (
+            cte.join(Order, region=cte.col.region_id)
+            .with_cte(cte)
+            .annotate(region_total=cte.col.total)
+        )
+
+        orders = orders_big.union(orders_small) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+             (10, 'mercury', 33),
+             (10, 'mercury', 99),
+             (11, 'mercury', 33),
+             (11, 'mercury', 99),
+             (12, 'mercury', 33),
+             (12, 'mercury', 99),
+             (20, 'venus', 86),
+             (20, 'venus', 258),
+             (21, 'venus', 86),
+             (21, 'venus', 258),
+             (22, 'venus', 86),
+             (22, 'venus', 258),
+             (23, 'venus', 86),
+             (23, 'venus', 258),
+             (30, 'earth', 126),
+             (30, 'earth', 378),
+             (31, 'earth', 126),
+             (31, 'earth', 378),
+             (32, 'earth', 126),
+             (32, 'earth', 378),
+             (33, 'earth', 126),
+             (33, 'earth', 378),
+             (40, 'mars', 123),
+             (40, 'mars', 369),
+             (41, 'mars', 123),
+             (41, 'mars', 369),
+             (42, 'mars', 123),
+             (42, 'mars', 369)
+        ])
+
+    def test_cte_intersection(self):
+        cte_big = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='big'
+        )
+        cte_small = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='small'
+        )
+        orders_big = (
+            cte_big.join(Order, region=cte_big.col.region_id)
+            .with_cte(cte_big)
+            .annotate(region_total=cte_big.col.total)
+            .filter(region_total__gte=86)
+        )
+        orders_small = (
+            cte_small.join(Order, region=cte_small.col.region_id)
+            .with_cte(cte_small)
+            .annotate(region_total=cte_small.col.total)
+            .filter(region_total__lte=123)
+        )
+
+        orders = orders_small.intersection(orders_big) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+            (20, 'venus', 86),
+            (21, 'venus', 86),
+            (22, 'venus', 86),
+            (23, 'venus', 86),
+            (40, 'mars', 123),
+            (41, 'mars', 123),
+            (42, 'mars', 123),
+        ])
+
+    def test_cte_difference(self):
+        cte_big = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='big'
+        )
+        cte_small = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name='small'
+        )
+        orders_big = (
+            cte_big.join(Order, region=cte_big.col.region_id)
+            .with_cte(cte_big)
+            .annotate(region_total=cte_big.col.total)
+            .filter(region_total__gte=86)
+        )
+        orders_small = (
+            cte_small.join(Order, region=cte_small.col.region_id)
+            .with_cte(cte_small)
+            .annotate(region_total=cte_small.col.total)
+            .filter(region_total__lte=123)
+        )
+
+        orders = orders_small.difference(orders_big) \
+            .values_list("amount", "region_id", "region_total")
+        print(orders.query)
+
+        self.assertEqual(sorted(orders), [
+            (1, 'moon', 6),
+            (2, 'moon', 6),
+            (3, 'moon', 6),
+            (10, 'mercury', 33),
+            (10, 'proxima centauri b', 33),
+            (11, 'mercury', 33),
+            (11, 'proxima centauri b', 33),
+            (12, 'mercury', 33),
+            (12, 'proxima centauri b', 33),
+        ])
diff -pruN 1.3.3-2/tests/test_v1/test_cte.py 2.0.0-0ubuntu1/tests/test_v1/test_cte.py
--- 1.3.3-2/tests/test_v1/test_cte.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_cte.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,642 @@
+from unittest import SkipTest
+
+from django.db.models import IntegerField, TextField
+from django.db.models.aggregates import Count, Max, Min, Sum
+from django.db.models.expressions import (
+    Exists, ExpressionWrapper, F, OuterRef, Subquery,
+)
+from django.db.models.sql.constants import LOUTER
+from django.test import TestCase
+
+from django_cte import With
+from django_cte import CTEManager
+
+from .models import Order, Region, User
+
+int_field = IntegerField()
+text_field = TextField()
+
+
+class TestCTE(TestCase):
+
+    def test_simple_cte_query(self):
+        cte = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount"))
+        )
+
+        orders = (
+            # FROM orders INNER JOIN cte ON orders.region_id = cte.region_id
+            cte.join(Order, region=cte.col.region_id)
+
+            # Add `WITH ...` before `SELECT ... FROM orders ...`
+            .with_cte(cte)
+
+            # Annotate each Order with a "region_total"
+            .annotate(region_total=cte.col.total)
+        )
+        print(orders.query)
+
+        data = sorted((o.amount, o.region_id, o.region_total) for o in orders)
+        self.assertEqual(data, [
+            (1, 'moon', 6),
+            (2, 'moon', 6),
+            (3, 'moon', 6),
+            (10, 'mercury', 33),
+            (10, 'proxima centauri b', 33),
+            (11, 'mercury', 33),
+            (11, 'proxima centauri b', 33),
+            (12, 'mercury', 33),
+            (12, 'proxima centauri b', 33),
+            (20, 'venus', 86),
+            (21, 'venus', 86),
+            (22, 'venus', 86),
+            (23, 'venus', 86),
+            (30, 'earth', 126),
+            (31, 'earth', 126),
+            (32, 'earth', 126),
+            (33, 'earth', 126),
+            (40, 'mars', 123),
+            (41, 'mars', 123),
+            (42, 'mars', 123),
+            (1000, 'sun', 1000),
+            (2000, 'proxima centauri', 2000),
+        ])
+
+    def test_cte_name_escape(self):
+        totals = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="mixedCaseCTEName"
+        )
+        orders = (
+            totals
+            .join(Order, region=totals.col.region_id)
+            .with_cte(totals)
+            .annotate(region_total=totals.col.total)
+            .order_by("amount")
+        )
+        self.assertTrue(
+            str(orders.query).startswith('WITH RECURSIVE "mixedCaseCTEName"'))
+
+    def test_cte_queryset(self):
+        sub_totals = With(
+            Order.objects
+            .values(region_parent=F("region__parent_id"))
+            .annotate(total=Sum("amount")),
+        )
+        regions = (
+            Region.objects.all()
+            .with_cte(sub_totals)
+            .annotate(
+                child_regions_total=Subquery(
+                    sub_totals.queryset()
+                    .filter(region_parent=OuterRef("name"))
+                    .values("total"),
+                ),
+            )
+            .order_by("name")
+        )
+        print(regions.query)
+
+        data = [(r.name, r.child_regions_total) for r in regions]
+        self.assertEqual(data, [
+            ("bernard's star", None),
+            ('deimos', None),
+            ('earth', 6),
+            ('mars', None),
+            ('mercury', None),
+            ('moon', None),
+            ('phobos', None),
+            ('proxima centauri', 33),
+            ('proxima centauri b', None),
+            ('sun', 368),
+            ('venus', None)
+        ])
+
+    def test_cte_queryset_with_model_result(self):
+        cte = With(
+            Order.objects
+            .annotate(region_parent=F("region__parent_id")),
+        )
+        orders = cte.queryset().with_cte(cte)
+        print(orders.query)
+
+        data = sorted(
+            (x.region_id, x.amount, x.region_parent) for x in orders)[:5]
+        self.assertEqual(data, [
+            ("earth", 30, "sun"),
+            ("earth", 31, "sun"),
+            ("earth", 32, "sun"),
+            ("earth", 33, "sun"),
+            ("mars", 40, "sun"),
+        ])
+        self.assertTrue(
+            all(isinstance(x, Order) for x in orders),
+            repr([x for x in orders]),
+        )
+
+    def test_cte_queryset_with_join(self):
+        cte = With(
+            Order.objects
+            .annotate(region_parent=F("region__parent_id")),
+        )
+        orders = (
+            cte.queryset()
+            .with_cte(cte)
+            .annotate(parent=F("region__parent_id"))
+            .order_by("region_id", "amount")
+        )
+        print(orders.query)
+
+        data = [(x.region_id, x.region_parent, x.parent) for x in orders][:5]
+        self.assertEqual(data, [
+            ("earth", "sun", "sun"),
+            ("earth", "sun", "sun"),
+            ("earth", "sun", "sun"),
+            ("earth", "sun", "sun"),
+            ("mars", "sun", "sun"),
+        ])
+
+    def test_cte_queryset_with_values_result(self):
+        cte = With(
+            Order.objects
+            .values(
+                "region_id",
+                region_parent=F("region__parent_id"),
+            )
+            .distinct()
+        )
+        values = (
+            cte.queryset()
+            .with_cte(cte)
+            .filter(region_parent__isnull=False)
+        )
+        print(values.query)
+
+        def key(item):
+            return item["region_parent"], item["region_id"]
+
+        data = sorted(values, key=key)[:5]
+        self.assertEqual(data, [
+            {'region_id': 'moon', 'region_parent': 'earth'},
+            {
+                'region_id': 'proxima centauri b',
+                'region_parent': 'proxima centauri',
+            },
+            {'region_id': 'earth', 'region_parent': 'sun'},
+            {'region_id': 'mars', 'region_parent': 'sun'},
+            {'region_id': 'mercury', 'region_parent': 'sun'},
+        ])
+
+    def test_named_simple_ctes(self):
+        totals = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="totals",
+        )
+        region_count = With(
+            Region.objects
+            .filter(parent="sun")
+            .values("parent_id")
+            .annotate(num=Count("name")),
+            name="region_count",
+        )
+        orders = (
+            region_count.join(
+                totals.join(Order, region=totals.col.region_id),
+                region__parent=region_count.col.parent_id
+            )
+            .with_cte(totals)
+            .with_cte(region_count)
+            .annotate(region_total=totals.col.total)
+            .annotate(region_count=region_count.col.num)
+            .order_by("amount")
+        )
+        print(orders.query)
+
+        data = [(
+            o.amount,
+            o.region_id,
+            o.region_count,
+            o.region_total,
+        ) for o in orders]
+        self.assertEqual(data, [
+            (10, 'mercury', 4, 33),
+            (11, 'mercury', 4, 33),
+            (12, 'mercury', 4, 33),
+            (20, 'venus', 4, 86),
+            (21, 'venus', 4, 86),
+            (22, 'venus', 4, 86),
+            (23, 'venus', 4, 86),
+            (30, 'earth', 4, 126),
+            (31, 'earth', 4, 126),
+            (32, 'earth', 4, 126),
+            (33, 'earth', 4, 126),
+            (40, 'mars', 4, 123),
+            (41, 'mars', 4, 123),
+            (42, 'mars', 4, 123),
+        ])
+
+    def test_named_ctes(self):
+        def make_root_mapping(rootmap):
+            return Region.objects.filter(
+                parent__isnull=True
+            ).values(
+                "name",
+                root=F("name"),
+            ).union(
+                rootmap.join(Region, parent=rootmap.col.name).values(
+                    "name",
+                    root=rootmap.col.root,
+                ),
+                all=True,
+            )
+        rootmap = With.recursive(make_root_mapping, name="rootmap")
+
+        totals = With(
+            rootmap.join(Order, region_id=rootmap.col.name)
+            .values(
+                root=rootmap.col.root,
+            ).annotate(
+                orders_count=Count("id"),
+                region_total=Sum("amount"),
+            ),
+            name="totals",
+        )
+
+        root_regions = (
+            totals.join(Region, name=totals.col.root)
+            .with_cte(rootmap)
+            .with_cte(totals)
+            .annotate(
+                # count of orders in this region and all subregions
+                orders_count=totals.col.orders_count,
+                # sum of order amounts in this region and all subregions
+                region_total=totals.col.region_total,
+            )
+        )
+        print(root_regions.query)
+
+        data = sorted(
+            (r.name, r.orders_count, r.region_total) for r in root_regions
+        )
+        self.assertEqual(data, [
+            ('proxima centauri', 4, 2033),
+            ('sun', 18, 1374),
+        ])
+
+    def test_materialized_option(self):
+        totals = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            materialized=True
+        )
+        orders = (
+            totals
+            .join(Order, region=totals.col.region_id)
+            .with_cte(totals)
+            .annotate(region_total=totals.col.total)
+            .order_by("amount")
+        )
+        self.assertTrue(
+            str(orders.query).startswith(
+                'WITH RECURSIVE "cte" AS MATERIALIZED'
+            )
+        )
+
+    def test_update_cte_query(self):
+        cte = With(
+            Order.objects
+            .values(region_parent=F("region__parent_id"))
+            .annotate(total=Sum("amount"))
+            .filter(total__isnull=False)
+        )
+        # not the most efficient query, but it exercises CTEUpdateQuery
+        Order.objects.all().with_cte(cte).filter(region_id__in=Subquery(
+            cte.queryset()
+            .filter(region_parent=OuterRef("region_id"))
+            .values("region_parent")
+        )).update(amount=Subquery(
+            cte.queryset()
+            .filter(region_parent=OuterRef("region_id"))
+            .values("total")
+        ))
+
+        data = set((o.region_id, o.amount) for o in Order.objects.filter(
+            region_id__in=["earth", "sun", "proxima centauri", "mars"]
+        ))
+        self.assertEqual(data, {
+            ('earth', 6),
+            ('mars', 40),
+            ('mars', 41),
+            ('mars', 42),
+            ('proxima centauri', 33),
+            ('sun', 368),
+        })
+
+    def test_update_with_subquery(self):
+        # Test for issue: https://github.com/dimagi/django-cte/issues/9
+        # Issue is not reproduces on sqlite3 use postgres to run.
+        # To reproduce the problem it's required to have some join
+        # in the select-query so the compiler will turn it into a subquery.
+        # To add a join use a filter over field of related model
+        orders = Order.objects.filter(region__parent_id='sun')
+        orders.update(amount=0)
+        data = {(order.region_id, order.amount) for order in orders}
+        self.assertEqual(data, {
+            ('mercury', 0),
+            ('venus', 0),
+            ('earth', 0),
+            ('mars', 0),
+        })
+
+    def test_delete_cte_query(self):
+        raise SkipTest(
+            "this test will not work until `QuerySet.delete` (Django method) "
+            "calls `self.query.chain(sql.DeleteQuery)` instead of "
+            "`sql.DeleteQuery(self.model)`"
+        )
+        cte = With(
+            Order.objects
+            .values(region_parent=F("region__parent_id"))
+            .annotate(total=Sum("amount"))
+            .filter(total__isnull=False)
+        )
+        Order.objects.all().with_cte(cte).annotate(
+            cte_has_order=Exists(
+                cte.queryset()
+                .values("total")
+                .filter(region_parent=OuterRef("region_id"))
+            )
+        ).filter(cte_has_order=False).delete()
+
+        data = [(o.region_id, o.amount) for o in Order.objects.all()]
+        self.assertEqual(data, [
+            ('sun', 1000),
+            ('earth', 30),
+            ('earth', 31),
+            ('earth', 32),
+            ('earth', 33),
+            ('proxima centauri', 2000),
+        ])
+
+    def test_outerref_in_cte_query(self):
+        # This query is meant to return the difference between min and max
+        # order of each region, through a subquery
+        min_and_max = With(
+            Order.objects
+            .filter(region=OuterRef("pk"))
+            .values('region')  # This is to force group by region_id
+            .annotate(
+                amount_min=Min("amount"),
+                amount_max=Max("amount"),
+            )
+            .values('amount_min', 'amount_max')
+        )
+        regions = (
+            Region.objects
+            .annotate(
+                difference=Subquery(
+                    min_and_max.queryset().with_cte(min_and_max).annotate(
+                        difference=ExpressionWrapper(
+                            F('amount_max') - F('amount_min'),
+                            output_field=int_field,
+                        ),
+                    ).values('difference')[:1],
+                    output_field=IntegerField()
+                )
+            )
+            .order_by("name")
+        )
+        print(regions.query)
+
+        data = [(r.name, r.difference) for r in regions]
+        self.assertEqual(data, [
+            ("bernard's star", None),
+            ('deimos', None),
+            ('earth', 3),
+            ('mars', 2),
+            ('mercury', 2),
+            ('moon', 2),
+            ('phobos', None),
+            ('proxima centauri', 0),
+            ('proxima centauri b', 2),
+            ('sun', 0),
+            ('venus', 3)
+        ])
+
+    def test_experimental_left_outer_join(self):
+        totals = With(
+            Order.objects
+            .values("region_id")
+            .annotate(total=Sum("amount"))
+            .filter(total__gt=100)
+        )
+        orders = (
+            totals
+            .join(Order, region=totals.col.region_id, _join_type=LOUTER)
+            .with_cte(totals)
+            .annotate(region_total=totals.col.total)
+        )
+        print(orders.query)
+        self.assertIn("LEFT OUTER JOIN", str(orders.query))
+        self.assertNotIn("INNER JOIN", str(orders.query))
+
+        data = sorted((o.region_id, o.amount, o.region_total) for o in orders)
+        self.assertEqual(data, [
+            ('earth', 30, 126),
+            ('earth', 31, 126),
+            ('earth', 32, 126),
+            ('earth', 33, 126),
+            ('mars', 40, 123),
+            ('mars', 41, 123),
+            ('mars', 42, 123),
+            ('mercury', 10, None),
+            ('mercury', 11, None),
+            ('mercury', 12, None),
+            ('moon', 1, None),
+            ('moon', 2, None),
+            ('moon', 3, None),
+            ('proxima centauri', 2000, 2000),
+            ('proxima centauri b', 10, None),
+            ('proxima centauri b', 11, None),
+            ('proxima centauri b', 12, None),
+            ('sun', 1000, 1000),
+            ('venus', 20, None),
+            ('venus', 21, None),
+            ('venus', 22, None),
+            ('venus', 23, None),
+        ])
+
+    def test_non_cte_subquery(self):
+        """
+        Verifies that subquery annotations are handled correctly when the
+        subquery model doesn't use the CTE manager, and the query results
+        match expected behavior
+        """
+        self.assertNotIsInstance(User.objects, CTEManager)
+
+        sub_totals = With(
+            Order.objects
+            .values(region_parent=F("region__parent_id"))
+            .annotate(
+                total=Sum("amount"),
+                # trivial subquery example testing existence of
+                # a user for the order
+                non_cte_subquery=Exists(
+                    User.objects.filter(pk=OuterRef("user_id"))
+                ),
+            ),
+        )
+        regions = (
+            Region.objects.all()
+            .with_cte(sub_totals)
+            .annotate(
+                child_regions_total=Subquery(
+                    sub_totals.queryset()
+                    .filter(region_parent=OuterRef("name"))
+                    .values("total"),
+                ),
+            )
+            .order_by("name")
+        )
+        print(regions.query)
+
+        data = [(r.name, r.child_regions_total) for r in regions]
+        self.assertEqual(data, [
+            ("bernard's star", None),
+            ('deimos', None),
+            ('earth', 6),
+            ('mars', None),
+            ('mercury', None),
+            ('moon', None),
+            ('phobos', None),
+            ('proxima centauri', 33),
+            ('proxima centauri b', None),
+            ('sun', 368),
+            ('venus', None)
+        ])
+
+    def test_explain(self):
+        """
+        Verifies that using .explain() prepends the EXPLAIN clause in the
+        correct position
+        """
+
+        totals = With(
+            Order.objects
+            .filter(region__parent="sun")
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="totals",
+        )
+        region_count = With(
+            Region.objects
+            .filter(parent="sun")
+            .values("parent_id")
+            .annotate(num=Count("name")),
+            name="region_count",
+        )
+        orders = (
+            region_count.join(
+                totals.join(Order, region=totals.col.region_id),
+                region__parent=region_count.col.parent_id
+            )
+            .with_cte(totals)
+            .with_cte(region_count)
+            .annotate(region_total=totals.col.total)
+            .annotate(region_count=region_count.col.num)
+            .order_by("amount")
+        )
+        print(orders.query)
+
+        self.assertIsInstance(orders.explain(), str)
+
+    def test_empty_result_set_cte(self):
+        """
+        Verifies that the CTEQueryCompiler can handle empty result sets in the
+        related CTEs
+        """
+        totals = With(
+            Order.objects
+            .filter(id__in=[])
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="totals",
+        )
+        orders = (
+            totals.join(Order, region=totals.col.region_id)
+            .with_cte(totals)
+            .annotate(region_total=totals.col.total)
+            .order_by("amount")
+        )
+
+        self.assertEqual(len(orders), 0)
+
+    def test_left_outer_join_on_empty_result_set_cte(self):
+        totals = With(
+            Order.objects
+            .filter(id__in=[])
+            .values("region_id")
+            .annotate(total=Sum("amount")),
+            name="totals",
+        )
+        orders = (
+            totals.join(Order, region=totals.col.region_id, _join_type=LOUTER)
+            .with_cte(totals)
+            .annotate(region_total=totals.col.total)
+            .order_by("amount")
+        )
+
+        self.assertEqual(len(orders), 22)
+
+    def test_union_query_with_cte(self):
+        orders = (
+            Order.objects
+            .filter(region__parent="sun")
+            .only("region", "amount")
+        )
+        orders_cte = With(orders, name="orders_cte")
+        orders_cte_queryset = orders_cte.queryset()
+
+        earth_orders = orders_cte_queryset.filter(region="earth")
+        mars_orders = orders_cte_queryset.filter(region="mars")
+
+        earth_mars = earth_orders.union(mars_orders, all=True)
+        earth_mars_cte = (
+            earth_mars
+            .with_cte(orders_cte)
+            .order_by("region", "amount")
+            .values_list("region", "amount")
+        )
+        print(earth_mars_cte.query)
+
+        self.assertEqual(list(earth_mars_cte), [
+            ('earth', 30),
+            ('earth', 31),
+            ('earth', 32),
+            ('earth', 33),
+            ('mars', 40),
+            ('mars', 41),
+            ('mars', 42),
+        ])
+
+    def test_cte_select_pk(self):
+        orders = Order.objects.filter(region="earth").values("pk")
+        cte = With(orders)
+        queryset = cte.join(orders, pk=cte.col.pk).with_cte(cte).order_by("pk")
+        print(queryset.query)
+        self.assertEqual(list(queryset), [
+            {'pk': 9},
+            {'pk': 10},
+            {'pk': 11},
+            {'pk': 12},
+        ])
diff -pruN 1.3.3-2/tests/test_v1/test_django.py 2.0.0-0ubuntu1/tests/test_v1/test_django.py
--- 1.3.3-2/tests/test_v1/test_django.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_django.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,87 @@
+from unittest import SkipTest
+
+import django
+from django.db import OperationalError, ProgrammingError
+from django.db.models import Window
+from django.db.models.functions import Rank
+from django.test import TestCase, skipUnlessDBFeature
+
+from .models import Order, Region, User
+
+
+@skipUnlessDBFeature("supports_select_union")
+class NonCteQueries(TestCase):
+    """Test non-CTE queries
+
+    These tests were adapted from the Django test suite. The models used
+    here use CTEManager and CTEQuerySet to verify feature parity with
+    their base classes Manager and QuerySet.
+    """
+
+    @classmethod
+    def setUpTestData(cls):
+        Order.objects.all().delete()
+
+    def test_union_with_select_related_and_order(self):
+        e1 = User.objects.create(name="e1")
+        a1 = Order.objects.create(region_id="earth", user=e1)
+        a2 = Order.objects.create(region_id="moon", user=e1)
+        Order.objects.create(region_id="sun", user=e1)
+        base_qs = Order.objects.select_related("user").order_by()
+        qs1 = base_qs.filter(region_id="earth")
+        qs2 = base_qs.filter(region_id="moon")
+        print(qs1.union(qs2).order_by("pk").query)
+        self.assertSequenceEqual(qs1.union(qs2).order_by("pk"), [a1, a2])
+
+    @skipUnlessDBFeature("supports_slicing_ordering_in_compound")
+    def test_union_with_select_related_and_first(self):
+        e1 = User.objects.create(name="e1")
+        a1 = Order.objects.create(region_id="earth", user=e1)
+        Order.objects.create(region_id="moon", user=e1)
+        base_qs = Order.objects.select_related("user")
+        qs1 = base_qs.filter(region_id="earth")
+        qs2 = base_qs.filter(region_id="moon")
+        self.assertEqual(qs1.union(qs2).first(), a1)
+
+    def test_union_with_first(self):
+        e1 = User.objects.create(name="e1")
+        a1 = Order.objects.create(region_id="earth", user=e1)
+        base_qs = Order.objects.order_by()
+        qs1 = base_qs.filter(region_id="earth")
+        qs2 = base_qs.filter(region_id="moon")
+        self.assertEqual(qs1.union(qs2).first(), a1)
+
+
+class WindowFunctions(TestCase):
+
+    def test_heterogeneous_filter_in_cte(self):
+        if django.VERSION < (4, 2):
+            raise SkipTest("feature added in Django 4.2")
+        from django_cte import With
+        cte = With(
+            Order.objects.annotate(
+                region_amount_rank=Window(
+                    Rank(), partition_by="region_id", order_by="-amount"
+                ),
+            )
+            .order_by("region_id")
+            .values("region_id", "region_amount_rank")
+            .filter(region_amount_rank=1, region_id__in=["sun", "moon"])
+        )
+        qs = cte.join(Region, name=cte.col.region_id).with_cte(cte)
+        print(qs.query)
+        # ProgrammingError: column cte.region_id does not exist
+        # WITH RECURSIVE "cte" AS (SELECT * FROM (
+        #   SELECT "orders"."region_id" AS "col1", ...
+        # "region" INNER JOIN "cte" ON "region"."name" = ("cte"."region_id")
+        try:
+            self.assertEqual({r.name for r in qs}, {"moon", "sun"})
+        except (OperationalError, ProgrammingError) as err:
+            if "cte.region_id" in str(err):
+                raise SkipTest(
+                    "window function auto-aliasing breaks CTE "
+                    "column references"
+                )
+            raise
+        if django.VERSION < (5, 2):
+            assert 0, "unexpected pass"
diff -pruN 1.3.3-2/tests/test_v1/test_manager.py 2.0.0-0ubuntu1/tests/test_v1/test_manager.py
--- 1.3.3-2/tests/test_v1/test_manager.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_manager.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,111 @@
+from django.db.models.expressions import F
+from django.db.models.query import QuerySet
+from django.test import TestCase
+
+from django_cte import With, CTEQuerySet, CTEManager
+
+from .models import (
+    Order,
+    OrderFromLT40,
+    OrderLT40AsManager,
+    OrderCustomManagerNQuery,
+    OrderCustomManager,
+    LT40QuerySet,
+    LTManager,
+    LT25QuerySet,
+)
+
+
+class TestCTE(TestCase):
+    def test_cte_queryset_correct_defaultmanager(self):
+        self.assertEqual(type(Order._default_manager), CTEManager)
+        self.assertEqual(type(Order.objects.all()), CTEQuerySet)
+
+    def test_cte_queryset_correct_from_queryset(self):
+        self.assertEqual(type(OrderFromLT40.objects.all()), LT40QuerySet)
+
+    def test_cte_queryset_correct_queryset_as_manager(self):
+        self.assertEqual(type(OrderLT40AsManager.objects.all()), LT40QuerySet)
+
+    def test_cte_queryset_correct_manager_n_from_queryset(self):
+        self.assertIsInstance(
+            OrderCustomManagerNQuery._default_manager, LTManager)
+        self.assertEqual(type(
+            OrderCustomManagerNQuery.objects.all()), LT25QuerySet)
+
+    def test_cte_create_manager_from_non_cteQuery(self):
+        class BrokenQuerySet(QuerySet):
+            "This should be a CTEQuerySet if we want this to work"
+
+        with self.assertRaises(TypeError):
+            CTEManager.from_queryset(BrokenQuerySet)()
+
+    def test_cte_queryset_correct_limitedmanager(self):
+        self.assertEqual(type(OrderCustomManager._default_manager), LTManager)
+        # Check the expected even if not ideal behavior occurs
+        self.assertIsInstance(OrderCustomManager.objects.all(), CTEQuerySet)
+
+    def test_cte_queryset_with_from_queryset(self):
+        self.assertEqual(type(OrderFromLT40.objects.all()), LT40QuerySet)
+
+        cte = With(
+            OrderFromLT40.objects
+            .annotate(region_parent=F("region__parent_id"))
+            .filter(region__parent_id="sun")
+        )
+        orders = (
+            cte.queryset()
+            .with_cte(cte)
+            .lt40()  # custom queryset method
+            .order_by("region_id", "amount")
+        )
+        print(orders.query)
+
+        data = [(x.region_id, x.amount, x.region_parent) for x in orders]
+        self.assertEqual(data, [
+            ("earth", 30, "sun"),
+            ("earth", 31, "sun"),
+            ("earth", 32, "sun"),
+            ("earth", 33, "sun"),
+            ('mercury', 10, 'sun'),
+            ('mercury', 11, 'sun'),
+            ('mercury', 12, 'sun'),
+            ('venus', 20, 'sun'),
+            ('venus', 21, 'sun'),
+            ('venus', 22, 'sun'),
+            ('venus', 23, 'sun'),
+        ])
+
+    def test_cte_queryset_with_custom_queryset(self):
+        cte = With(
+            OrderCustomManagerNQuery.objects
+            .annotate(region_parent=F("region__parent_id"))
+            .filter(region__parent_id="sun")
+        )
+        orders = (
+            cte.queryset()
+            .with_cte(cte)
+            .lt25()  # custom queryset method
+            .order_by("region_id", "amount")
+        )
+        print(orders.query)
+
+        data = [(x.region_id, x.amount, x.region_parent) for x in orders]
+        self.assertEqual(data, [
+            ('mercury', 10, 'sun'),
+            ('mercury', 11, 'sun'),
+            ('mercury', 12, 'sun'),
+            ('venus', 20, 'sun'),
+            ('venus', 21, 'sun'),
+            ('venus', 22, 'sun'),
+            ('venus', 23, 'sun'),
+        ])
+
+    def test_cte_queryset_with_deferred_loading(self):
+        cte = With(
+            OrderCustomManagerNQuery.objects.order_by("id").only("id")[:1]
+        )
+        orders = cte.queryset().with_cte(cte)
+        print(orders.query)
+
+        self.assertEqual([x.id for x in orders], [1])
diff -pruN 1.3.3-2/tests/test_v1/test_raw.py 2.0.0-0ubuntu1/tests/test_v1/test_raw.py
--- 1.3.3-2/tests/test_v1/test_raw.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_raw.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,60 @@
+from django.db.models import IntegerField, TextField
+from django.test import TestCase
+
+from django_cte import With
+from django_cte.raw import raw_cte_sql
+
+from .models import Region
+
+int_field = IntegerField()
+text_field = TextField()
+
+
+class TestRawCTE(TestCase):
+
+    def test_raw_cte_sql(self):
+        cte = With(raw_cte_sql(
+            """
+            SELECT region_id, AVG(amount) AS avg_order
+            FROM orders
+            WHERE region_id = %s
+            GROUP BY region_id
+            """,
+            ["moon"],
+            {"region_id": text_field, "avg_order": int_field},
+        ))
+        moon_avg = (
+            cte
+            .join(Region, name=cte.col.region_id)
+            .annotate(avg_order=cte.col.avg_order)
+            .with_cte(cte)
+        )
+        print(moon_avg.query)
+
+        data = [(r.name, r.parent.name, r.avg_order) for r in moon_avg]
+        self.assertEqual(data, [('moon', 'earth', 2)])
+
+    def test_raw_cte_sql_name_escape(self):
+        cte = With(
+            raw_cte_sql(
+                """
+                SELECT region_id, AVG(amount) AS avg_order
+                FROM orders
+                WHERE region_id = %s
+                GROUP BY region_id
+                """,
+                ["moon"],
+                {"region_id": text_field, "avg_order": int_field},
+            ),
+            name="mixedCaseCTEName"
+        )
+        moon_avg = (
+            cte
+            .join(Region, name=cte.col.region_id)
+            .annotate(avg_order=cte.col.avg_order)
+            .with_cte(cte)
+        )
+        self.assertTrue(
+            str(moon_avg.query).startswith(
+                'WITH RECURSIVE "mixedCaseCTEName"')
+        )
diff -pruN 1.3.3-2/tests/test_v1/test_recursive.py 2.0.0-0ubuntu1/tests/test_v1/test_recursive.py
--- 1.3.3-2/tests/test_v1/test_recursive.py	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/tests/test_v1/test_recursive.py	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,335 @@
+import pickle
+from unittest import SkipTest
+
+from django.db.models import IntegerField, TextField
+from django.db.models.expressions import (
+    Case,
+    Exists,
+    ExpressionWrapper,
+    F,
+    OuterRef,
+    Q,
+    Value,
+    When,
+)
+from django.db.models.functions import Concat
+from django.db.utils import DatabaseError
+from django.test import TestCase
+
+from django_cte import With
+
+from .models import KeyPair, Region
+
+int_field = IntegerField()
+text_field = TextField()
+
+
+class TestRecursiveCTE(TestCase):
+
+    def test_recursive_cte_query(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                # non-recursive: get root nodes
+                parent__isnull=True
+            ).values(
+                "name",
+                path=F("name"),
+                depth=Value(0, output_field=int_field),
+            ).union(
+                # recursive union: get descendants
+                cte.join(Region, parent=cte.col.name).values(
+                    "name",
+                    path=Concat(
+                        cte.col.path, Value(" / "), F("name"),
+                        output_field=text_field,
+                    ),
+                    depth=cte.col.depth + Value(1, output_field=int_field),
+                ),
+                all=True,
+            )
+
+        cte = With.recursive(make_regions_cte)
+
+        regions = (
+            cte.join(Region, name=cte.col.name)
+            .with_cte(cte)
+            .annotate(
+                path=cte.col.path,
+                depth=cte.col.depth,
+            )
+            .filter(depth=2)
+            .order_by("path")
+        )
+        print(regions.query)
+
+        data = [(r.name, r.path, r.depth) for r in regions]
+        self.assertEqual(data, [
+            ('moon', 'sun / earth / moon', 2),
+            ('deimos', 'sun / mars / deimos', 2),
+            ('phobos', 'sun / mars / phobos', 2),
+        ])
+
+    def test_recursive_cte_reference_in_condition(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                parent__isnull=True
+            ).values(
+                "name",
+                path=F("name"),
+                depth=Value(0, output_field=int_field),
+                is_planet=Value(0, output_field=int_field),
+            ).union(
+                cte.join(
+                    Region, parent=cte.col.name
+                ).annotate(
+                    # annotations for filter and CASE/WHEN conditions
+                    parent_name=ExpressionWrapper(
+                        cte.col.name,
+                        output_field=text_field,
+                    ),
+                    parent_depth=ExpressionWrapper(
+                        cte.col.depth,
+                        output_field=int_field,
+                    ),
+                ).filter(
+                    ~Q(parent_name="mars"),
+                ).values(
+                    "name",
+                    path=Concat(
+                        cte.col.path, Value("\x01"), F("name"),
+                        output_field=text_field,
+                    ),
+                    depth=cte.col.depth + Value(1, output_field=int_field),
+                    is_planet=Case(
+                        When(parent_depth=0, then=Value(1)),
+                        default=Value(0),
+                        output_field=int_field,
+                    ),
+                ),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        regions = cte.join(Region, name=cte.col.name).with_cte(cte).annotate(
+            path=cte.col.path,
+            depth=cte.col.depth,
+            is_planet=cte.col.is_planet,
+        ).order_by("path")
+
+        data = [(r.path.split("\x01"), r.is_planet) for r in regions]
+        print(data)
+        self.assertEqual(data, [
+            (["bernard's star"], 0),
+            (['proxima centauri'], 0),
+            (['proxima centauri', 'proxima centauri b'], 1),
+            (['sun'], 0),
+            (['sun', 'earth'], 1),
+            (['sun', 'earth', 'moon'], 0),
+            (['sun', 'mars'], 1),  # mars moons excluded: parent_name != 'mars'
+            (['sun', 'mercury'], 1),
+            (['sun', 'venus'], 1),
+        ])
+
+    def test_recursive_cte_with_empty_union_part(self):
+        def make_regions_cte(cte):
+            return Region.objects.none().union(
+                cte.join(Region, parent=cte.col.name),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        regions = cte.join(Region, name=cte.col.name).with_cte(cte)
+
+        print(regions.query)
+        try:
+            self.assertEqual(regions.count(), 0)
+        except DatabaseError:
+            raise SkipTest(
+                "Expected failure: QuerySet omits `EmptyQuerySet` from "
+                "UNION queries resulting in invalid CTE SQL"
+            )
+
+        # -- recursive query "cte" does not have the form
+        # -- non-recursive-term UNION [ALL] recursive-term
+        # WITH RECURSIVE cte AS (
+        #   SELECT "tests_region"."name", "tests_region"."parent_id"
+        #   FROM "tests_region", "cte"
+        #   WHERE "tests_region"."parent_id" = ("cte"."name")
+        # )
+        # SELECT COUNT(*)
+        # FROM "tests_region", "cte"
+        # WHERE "tests_region"."name" = ("cte"."name")
+
+    def test_circular_ref_error(self):
+        def make_bad_cte(cte):
+            # NOTE: not a valid recursive CTE query
+            return cte.join(Region, parent=cte.col.name).values(
+                depth=cte.col.depth + 1,
+            )
+        cte = With.recursive(make_bad_cte)
+        regions = cte.join(Region, name=cte.col.name).with_cte(cte)
+        with self.assertRaises(ValueError) as context:
+            print(regions.query)
+        self.assertIn("Circular reference:", str(context.exception))
+
+    def test_attname_should_not_mask_col_name(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                name="moon"
+            ).values(
+                "name",
+                "parent_id",
+            ).union(
+                cte.join(Region, name=cte.col.parent_id).values(
+                    "name",
+                    "parent_id",
+                ),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        regions = (
+            Region.objects.all()
+            .with_cte(cte)
+            .annotate(_ex=Exists(
+                cte.queryset()
+                .values(value=Value("1", output_field=int_field))
+                .filter(name=OuterRef("name"))
+            ))
+            .filter(_ex=True)
+            .order_by("name")
+        )
+        print(regions.query)
+
+        data = [r.name for r in regions]
+        self.assertEqual(data, ['earth', 'moon', 'sun'])
+
+    def test_pickle_recursive_cte_queryset(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                parent__isnull=True
+            ).annotate(
+                depth=Value(0, output_field=int_field),
+            ).union(
+                cte.join(Region, parent=cte.col.name).annotate(
+                    depth=cte.col.depth + Value(1, output_field=int_field),
+                ),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        regions = cte.queryset().with_cte(cte).filter(depth=2).order_by("name")
+
+        pickled_qs = pickle.loads(pickle.dumps(regions))
+
+        data = [(r.name, r.depth) for r in pickled_qs]
+        self.assertEqual(data, [(r.name, r.depth) for r in regions])
+        self.assertEqual(data, [('deimos', 2), ('moon', 2), ('phobos', 2)])
+
+    def test_alias_change_in_annotation(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                parent__name="sun",
+            ).annotate(
+                value=F('name'),
+            ).union(
+                cte.join(
+                    Region.objects.all().annotate(
+                        value=F('name'),
+                    ),
+                    parent_id=cte.col.name,
+                ),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        query = cte.queryset().with_cte(cte)
+
+        exclude_leaves = With(cte.queryset().filter(
+            parent__name='sun',
+        ).annotate(
+            value=Concat(F('name'), F('name'))
+        ), name='value_cte')
+
+        query = query.annotate(
+            _exclude_leaves=Exists(
+                exclude_leaves.queryset().filter(
+                    name=OuterRef("name"),
+                    value=OuterRef("value"),
+                )
+            )
+        ).filter(_exclude_leaves=True).with_cte(exclude_leaves)
+        print(query.query)
+
+        # Nothing should be returned.
+        self.assertFalse(query)
+
+    def test_alias_as_subquery(self):
+        # This test covers CTEColumnRef.relabeled_clone
+        def make_regions_cte(cte):
+            return KeyPair.objects.filter(
+                parent__key="level 1",
+            ).annotate(
+                rank=F('value'),
+            ).union(
+                cte.join(
+                    KeyPair.objects.all().order_by(),
+                    parent_id=cte.col.id,
+                ).annotate(
+                    rank=F('value'),
+                ),
+                all=True,
+            )
+        cte = With.recursive(make_regions_cte)
+        children = cte.queryset().with_cte(cte)
+
+        xdups = With(cte.queryset().filter(
+            parent__key="level 1",
+        ).annotate(
+            rank=F('value')
+        ).values('id', 'rank'), name='xdups')
+
+        children = children.annotate(
+            _exclude=Exists(
+                (
+                    xdups.queryset().filter(
+                        id=OuterRef("id"),
+                        rank=OuterRef("rank"),
+                    )
+                )
+            )
+        ).filter(_exclude=True).with_cte(xdups)
+
+        print(children.query)
+        query = KeyPair.objects.filter(parent__in=children)
+        print(query.query)
+        print(children.query)
+        self.assertEqual(query.get().key, 'level 3')
+        # Tests the case in which children's query was modified since it was
+        # used in a subquery to define `query` above.
+        self.assertEqual(
+            list(c.key for c in children),
+            ['level 2', 'level 2']
+        )
+
+    def test_materialized(self):
+        # This test covers MATERIALIZED option in SQL query
+        def make_regions_cte(cte):
+            return KeyPair.objects.all()
+        cte = With.recursive(make_regions_cte, materialized=True)
+
+        query = KeyPair.objects.with_cte(cte)
+        print(query.query)
+        self.assertTrue(
+            str(query.query).startswith('WITH RECURSIVE "cte" AS MATERIALIZED')
+        )
+
+    def test_recursive_self_queryset(self):
+        def make_regions_cte(cte):
+            return Region.objects.filter(
+                pk="earth"
+            ).values("pk").union(
+                cte.join(Region, parent=cte.col.pk).values("pk")
+            )
+        cte = With.recursive(make_regions_cte)
+        queryset = cte.queryset().with_cte(cte).order_by("pk")
+        print(queryset.query)
+        self.assertEqual(list(queryset), [
+            {'pk': 'earth'},
+            {'pk': 'moon'},
+        ])
diff -pruN 1.3.3-2/uv.lock 2.0.0-0ubuntu1/uv.lock
--- 1.3.3-2/uv.lock	1970-01-01 00:00:00.000000000 +0000
+++ 2.0.0-0ubuntu1/uv.lock	2025-06-16 15:26:31.000000000 +0000
@@ -0,0 +1,291 @@
+version = 1
+revision = 2
+requires-python = ">=3.9"
+resolution-markers = [
+    "python_full_version >= '3.10'",
+    "python_full_version < '3.10'",
+]
+
+[[package]]
+name = "asgiref"
+version = "3.8.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/29/38/b3395cc9ad1b56d2ddac9970bc8f4141312dbaec28bc7c218b0dfafd0f42/asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590", size = 35186, upload-time = "2024-03-22T14:39:36.863Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/39/e3/893e8757be2612e6c266d9bb58ad2e3651524b5b40cf56761e985a28b13e/asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47", size = 23828, upload-time = "2024-03-22T14:39:34.521Z" },
+]
+
+[[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 = "django"
+version = "4.2.21"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "asgiref" },
+    { name = "sqlparse" },
+    { name = "tzdata", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c1/bb/2fad5edc1af2945cb499a2e322ac28e4714fc310bd5201ed1f5a9f73a342/django-4.2.21.tar.gz", hash = "sha256:b54ac28d6aa964fc7c2f7335138a54d78980232011e0cd2231d04eed393dcb0d", size = 10424638, upload-time = "2025-05-07T14:07:07.992Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/4f/aeaa3098da18b625ed672f3da6d1cd94e188d1b2cc27c2c841b2f9666282/django-4.2.21-py3-none-any.whl", hash = "sha256:1d658c7bf5d31c7d0cac1cab58bc1f822df89255080fec81909256c30e6180b3", size = 7993839, upload-time = "2025-05-07T14:07:01.318Z" },
+]
+
+[[package]]
+name = "django-cte"
+source = { editable = "." }
+dependencies = [
+    { name = "django" },
+]
+
+[package.dev-dependencies]
+dev = [
+    { name = "psycopg2-binary" },
+    { name = "pytest-unmagic" },
+    { name = "ruff" },
+]
+
+[package.metadata]
+requires-dist = [{ name = "django" }]
+
+[package.metadata.requires-dev]
+dev = [
+    { name = "psycopg2-binary" },
+    { name = "pytest-unmagic" },
+    { name = "ruff" },
+]
+
+[[package]]
+name = "exceptiongroup"
+version = "1.3.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "typing-extensions", marker = "python_full_version < '3.13'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" },
+]
+
+[[package]]
+name = "iniconfig"
+version = "2.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "25.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" },
+]
+
+[[package]]
+name = "pluggy"
+version = "1.6.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
+]
+
+[[package]]
+name = "psycopg2-binary"
+version = "2.9.10"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/bdc8274dc0585090b4e3432267d7be4dfbfd8971c0fa59167c711105a6bf/psycopg2-binary-2.9.10.tar.gz", hash = "sha256:4b3df0e6990aa98acda57d983942eff13d824135fe2250e6522edaa782a06de2", size = 385764, upload-time = "2024-10-16T11:24:58.126Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/7a/81/331257dbf2801cdb82105306042f7a1637cc752f65f2bb688188e0de5f0b/psycopg2_binary-2.9.10-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:0ea8e3d0ae83564f2fc554955d327fa081d065c8ca5cc6d2abb643e2c9c1200f", size = 3043397, upload-time = "2024-10-16T11:18:58.647Z" },
+    { url = "https://files.pythonhosted.org/packages/e7/9a/7f4f2f031010bbfe6a02b4a15c01e12eb6b9b7b358ab33229f28baadbfc1/psycopg2_binary-2.9.10-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:3e9c76f0ac6f92ecfc79516a8034a544926430f7b080ec5a0537bca389ee0906", size = 3274806, upload-time = "2024-10-16T11:19:03.935Z" },
+    { url = "https://files.pythonhosted.org/packages/e5/57/8ddd4b374fa811a0b0a0f49b6abad1cde9cb34df73ea3348cc283fcd70b4/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ad26b467a405c798aaa1458ba09d7e2b6e5f96b1ce0ac15d82fd9f95dc38a92", size = 2851361, upload-time = "2024-10-16T11:19:07.277Z" },
+    { url = "https://files.pythonhosted.org/packages/f9/66/d1e52c20d283f1f3a8e7e5c1e06851d432f123ef57b13043b4f9b21ffa1f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:270934a475a0e4b6925b5f804e3809dd5f90f8613621d062848dd82f9cd62007", size = 3080836, upload-time = "2024-10-16T11:19:11.033Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/cb/592d44a9546aba78f8a1249021fe7c59d3afb8a0ba51434d6610cc3462b6/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:48b338f08d93e7be4ab2b5f1dbe69dc5e9ef07170fe1f86514422076d9c010d0", size = 3264552, upload-time = "2024-10-16T11:19:14.606Z" },
+    { url = "https://files.pythonhosted.org/packages/64/33/c8548560b94b7617f203d7236d6cdf36fe1a5a3645600ada6efd79da946f/psycopg2_binary-2.9.10-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7f4152f8f76d2023aac16285576a9ecd2b11a9895373a1f10fd9db54b3ff06b4", size = 3019789, upload-time = "2024-10-16T11:19:18.889Z" },
+    { url = "https://files.pythonhosted.org/packages/b0/0e/c2da0db5bea88a3be52307f88b75eec72c4de62814cbe9ee600c29c06334/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:32581b3020c72d7a421009ee1c6bf4a131ef5f0a968fab2e2de0c9d2bb4577f1", size = 2871776, upload-time = "2024-10-16T11:19:23.023Z" },
+    { url = "https://files.pythonhosted.org/packages/15/d7/774afa1eadb787ddf41aab52d4c62785563e29949613c958955031408ae6/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:2ce3e21dc3437b1d960521eca599d57408a695a0d3c26797ea0f72e834c7ffe5", size = 2820959, upload-time = "2024-10-16T11:19:26.906Z" },
+    { url = "https://files.pythonhosted.org/packages/5e/ed/440dc3f5991a8c6172a1cde44850ead0e483a375277a1aef7cfcec00af07/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:e984839e75e0b60cfe75e351db53d6db750b00de45644c5d1f7ee5d1f34a1ce5", size = 2919329, upload-time = "2024-10-16T11:19:30.027Z" },
+    { url = "https://files.pythonhosted.org/packages/03/be/2cc8f4282898306732d2ae7b7378ae14e8df3c1231b53579efa056aae887/psycopg2_binary-2.9.10-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3c4745a90b78e51d9ba06e2088a2fe0c693ae19cc8cb051ccda44e8df8a6eb53", size = 2957659, upload-time = "2024-10-16T11:19:32.864Z" },
+    { url = "https://files.pythonhosted.org/packages/d0/12/fb8e4f485d98c570e00dad5800e9a2349cfe0f71a767c856857160d343a5/psycopg2_binary-2.9.10-cp310-cp310-win32.whl", hash = "sha256:e5720a5d25e3b99cd0dc5c8a440570469ff82659bb09431c1439b92caf184d3b", size = 1024605, upload-time = "2024-10-16T11:19:35.462Z" },
+    { url = "https://files.pythonhosted.org/packages/22/4f/217cd2471ecf45d82905dd09085e049af8de6cfdc008b6663c3226dc1c98/psycopg2_binary-2.9.10-cp310-cp310-win_amd64.whl", hash = "sha256:3c18f74eb4386bf35e92ab2354a12c17e5eb4d9798e4c0ad3a00783eae7cd9f1", size = 1163817, upload-time = "2024-10-16T11:19:37.384Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/8f/9feb01291d0d7a0a4c6a6bab24094135c2b59c6a81943752f632c75896d6/psycopg2_binary-2.9.10-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:04392983d0bb89a8717772a193cfaac58871321e3ec69514e1c4e0d4957b5aff", size = 3043397, upload-time = "2024-10-16T11:19:40.033Z" },
+    { url = "https://files.pythonhosted.org/packages/15/30/346e4683532011561cd9c8dfeac6a8153dd96452fee0b12666058ab7893c/psycopg2_binary-2.9.10-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:1a6784f0ce3fec4edc64e985865c17778514325074adf5ad8f80636cd029ef7c", size = 3274806, upload-time = "2024-10-16T11:19:43.5Z" },
+    { url = "https://files.pythonhosted.org/packages/66/6e/4efebe76f76aee7ec99166b6c023ff8abdc4e183f7b70913d7c047701b79/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b5f86c56eeb91dc3135b3fd8a95dc7ae14c538a2f3ad77a19645cf55bab1799c", size = 2851370, upload-time = "2024-10-16T11:19:46.986Z" },
+    { url = "https://files.pythonhosted.org/packages/7f/fd/ff83313f86b50f7ca089b161b8e0a22bb3c319974096093cd50680433fdb/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2b3d2491d4d78b6b14f76881905c7a8a8abcf974aad4a8a0b065273a0ed7a2cb", size = 3080780, upload-time = "2024-10-16T11:19:50.242Z" },
+    { url = "https://files.pythonhosted.org/packages/e6/c4/bfadd202dcda8333a7ccafdc51c541dbdfce7c2c7cda89fa2374455d795f/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2286791ececda3a723d1910441c793be44625d86d1a4e79942751197f4d30341", size = 3264583, upload-time = "2024-10-16T11:19:54.424Z" },
+    { url = "https://files.pythonhosted.org/packages/5d/f1/09f45ac25e704ac954862581f9f9ae21303cc5ded3d0b775532b407f0e90/psycopg2_binary-2.9.10-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:512d29bb12608891e349af6a0cccedce51677725a921c07dba6342beaf576f9a", size = 3019831, upload-time = "2024-10-16T11:19:57.762Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/2e/9beaea078095cc558f215e38f647c7114987d9febfc25cb2beed7c3582a5/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5a507320c58903967ef7384355a4da7ff3f28132d679aeb23572753cbf2ec10b", size = 2871822, upload-time = "2024-10-16T11:20:04.693Z" },
+    { url = "https://files.pythonhosted.org/packages/01/9e/ef93c5d93f3dc9fc92786ffab39e323b9aed066ba59fdc34cf85e2722271/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6d4fa1079cab9018f4d0bd2db307beaa612b0d13ba73b5c6304b9fe2fb441ff7", size = 2820975, upload-time = "2024-10-16T11:20:11.401Z" },
+    { url = "https://files.pythonhosted.org/packages/a5/f0/049e9631e3268fe4c5a387f6fc27e267ebe199acf1bc1bc9cbde4bd6916c/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:851485a42dbb0bdc1edcdabdb8557c09c9655dfa2ca0460ff210522e073e319e", size = 2919320, upload-time = "2024-10-16T11:20:17.959Z" },
+    { url = "https://files.pythonhosted.org/packages/dc/9a/bcb8773b88e45fb5a5ea8339e2104d82c863a3b8558fbb2aadfe66df86b3/psycopg2_binary-2.9.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:35958ec9e46432d9076286dda67942ed6d968b9c3a6a2fd62b48939d1d78bf68", size = 2957617, upload-time = "2024-10-16T11:20:24.711Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/6b/144336a9bf08a67d217b3af3246abb1d027095dab726f0687f01f43e8c03/psycopg2_binary-2.9.10-cp311-cp311-win32.whl", hash = "sha256:ecced182e935529727401b24d76634a357c71c9275b356efafd8a2a91ec07392", size = 1024618, upload-time = "2024-10-16T11:20:27.718Z" },
+    { url = "https://files.pythonhosted.org/packages/61/69/3b3d7bd583c6d3cbe5100802efa5beacaacc86e37b653fc708bf3d6853b8/psycopg2_binary-2.9.10-cp311-cp311-win_amd64.whl", hash = "sha256:ee0e8c683a7ff25d23b55b11161c2663d4b099770f6085ff0a20d4505778d6b4", size = 1163816, upload-time = "2024-10-16T11:20:30.777Z" },
+    { url = "https://files.pythonhosted.org/packages/49/7d/465cc9795cf76f6d329efdafca74693714556ea3891813701ac1fee87545/psycopg2_binary-2.9.10-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:880845dfe1f85d9d5f7c412efea7a08946a46894537e4e5d091732eb1d34d9a0", size = 3044771, upload-time = "2024-10-16T11:20:35.234Z" },
+    { url = "https://files.pythonhosted.org/packages/8b/31/6d225b7b641a1a2148e3ed65e1aa74fc86ba3fee850545e27be9e1de893d/psycopg2_binary-2.9.10-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:9440fa522a79356aaa482aa4ba500b65f28e5d0e63b801abf6aa152a29bd842a", size = 3275336, upload-time = "2024-10-16T11:20:38.742Z" },
+    { url = "https://files.pythonhosted.org/packages/30/b7/a68c2b4bff1cbb1728e3ec864b2d92327c77ad52edcd27922535a8366f68/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e3923c1d9870c49a2d44f795df0c889a22380d36ef92440ff618ec315757e539", size = 2851637, upload-time = "2024-10-16T11:20:42.145Z" },
+    { url = "https://files.pythonhosted.org/packages/0b/b1/cfedc0e0e6f9ad61f8657fd173b2f831ce261c02a08c0b09c652b127d813/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7b2c956c028ea5de47ff3a8d6b3cc3330ab45cf0b7c3da35a2d6ff8420896526", size = 3082097, upload-time = "2024-10-16T11:20:46.185Z" },
+    { url = "https://files.pythonhosted.org/packages/18/ed/0a8e4153c9b769f59c02fb5e7914f20f0b2483a19dae7bf2db54b743d0d0/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f758ed67cab30b9a8d2833609513ce4d3bd027641673d4ebc9c067e4d208eec1", size = 3264776, upload-time = "2024-10-16T11:20:50.879Z" },
+    { url = "https://files.pythonhosted.org/packages/10/db/d09da68c6a0cdab41566b74e0a6068a425f077169bed0946559b7348ebe9/psycopg2_binary-2.9.10-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8cd9b4f2cfab88ed4a9106192de509464b75a906462fb846b936eabe45c2063e", size = 3020968, upload-time = "2024-10-16T11:20:56.819Z" },
+    { url = "https://files.pythonhosted.org/packages/94/28/4d6f8c255f0dfffb410db2b3f9ac5218d959a66c715c34cac31081e19b95/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dc08420625b5a20b53551c50deae6e231e6371194fa0651dbe0fb206452ae1f", size = 2872334, upload-time = "2024-10-16T11:21:02.411Z" },
+    { url = "https://files.pythonhosted.org/packages/05/f7/20d7bf796593c4fea95e12119d6cc384ff1f6141a24fbb7df5a668d29d29/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d7cd730dfa7c36dbe8724426bf5612798734bff2d3c3857f36f2733f5bfc7c00", size = 2822722, upload-time = "2024-10-16T11:21:09.01Z" },
+    { url = "https://files.pythonhosted.org/packages/4d/e4/0c407ae919ef626dbdb32835a03b6737013c3cc7240169843965cada2bdf/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:155e69561d54d02b3c3209545fb08938e27889ff5a10c19de8d23eb5a41be8a5", size = 2920132, upload-time = "2024-10-16T11:21:16.339Z" },
+    { url = "https://files.pythonhosted.org/packages/2d/70/aa69c9f69cf09a01da224909ff6ce8b68faeef476f00f7ec377e8f03be70/psycopg2_binary-2.9.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3cc28a6fd5a4a26224007712e79b81dbaee2ffb90ff406256158ec4d7b52b47", size = 2959312, upload-time = "2024-10-16T11:21:25.584Z" },
+    { url = "https://files.pythonhosted.org/packages/d3/bd/213e59854fafe87ba47814bf413ace0dcee33a89c8c8c814faca6bc7cf3c/psycopg2_binary-2.9.10-cp312-cp312-win32.whl", hash = "sha256:ec8a77f521a17506a24a5f626cb2aee7850f9b69a0afe704586f63a464f3cd64", size = 1025191, upload-time = "2024-10-16T11:21:29.912Z" },
+    { url = "https://files.pythonhosted.org/packages/92/29/06261ea000e2dc1e22907dbbc483a1093665509ea586b29b8986a0e56733/psycopg2_binary-2.9.10-cp312-cp312-win_amd64.whl", hash = "sha256:18c5ee682b9c6dd3696dad6e54cc7ff3a1a9020df6a5c0f861ef8bfd338c3ca0", size = 1164031, upload-time = "2024-10-16T11:21:34.211Z" },
+    { url = "https://files.pythonhosted.org/packages/3e/30/d41d3ba765609c0763505d565c4d12d8f3c79793f0d0f044ff5a28bf395b/psycopg2_binary-2.9.10-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:26540d4a9a4e2b096f1ff9cce51253d0504dca5a85872c7f7be23be5a53eb18d", size = 3044699, upload-time = "2024-10-16T11:21:42.841Z" },
+    { url = "https://files.pythonhosted.org/packages/35/44/257ddadec7ef04536ba71af6bc6a75ec05c5343004a7ec93006bee66c0bc/psycopg2_binary-2.9.10-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e217ce4d37667df0bc1c397fdcd8de5e81018ef305aed9415c3b093faaeb10fb", size = 3275245, upload-time = "2024-10-16T11:21:51.989Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/11/48ea1cd11de67f9efd7262085588790a95d9dfcd9b8a687d46caf7305c1a/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:245159e7ab20a71d989da00f280ca57da7641fa2cdcf71749c193cea540a74f7", size = 2851631, upload-time = "2024-10-16T11:21:57.584Z" },
+    { url = "https://files.pythonhosted.org/packages/62/e0/62ce5ee650e6c86719d621a761fe4bc846ab9eff8c1f12b1ed5741bf1c9b/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c4ded1a24b20021ebe677b7b08ad10bf09aac197d6943bfe6fec70ac4e4690d", size = 3082140, upload-time = "2024-10-16T11:22:02.005Z" },
+    { url = "https://files.pythonhosted.org/packages/27/ce/63f946c098611f7be234c0dd7cb1ad68b0b5744d34f68062bb3c5aa510c8/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3abb691ff9e57d4a93355f60d4f4c1dd2d68326c968e7db17ea96df3c023ef73", size = 3264762, upload-time = "2024-10-16T11:22:06.412Z" },
+    { url = "https://files.pythonhosted.org/packages/43/25/c603cd81402e69edf7daa59b1602bd41eb9859e2824b8c0855d748366ac9/psycopg2_binary-2.9.10-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8608c078134f0b3cbd9f89b34bd60a943b23fd33cc5f065e8d5f840061bd0673", size = 3020967, upload-time = "2024-10-16T11:22:11.583Z" },
+    { url = "https://files.pythonhosted.org/packages/5f/d6/8708d8c6fca531057fa170cdde8df870e8b6a9b136e82b361c65e42b841e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:230eeae2d71594103cd5b93fd29d1ace6420d0b86f4778739cb1a5a32f607d1f", size = 2872326, upload-time = "2024-10-16T11:22:16.406Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/ac/5b1ea50fc08a9df82de7e1771537557f07c2632231bbab652c7e22597908/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:bb89f0a835bcfc1d42ccd5f41f04870c1b936d8507c6df12b7737febc40f0909", size = 2822712, upload-time = "2024-10-16T11:22:21.366Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/fc/504d4503b2abc4570fac3ca56eb8fed5e437bf9c9ef13f36b6621db8ef00/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:f0c2d907a1e102526dd2986df638343388b94c33860ff3bbe1384130828714b1", size = 2920155, upload-time = "2024-10-16T11:22:25.684Z" },
+    { url = "https://files.pythonhosted.org/packages/b2/d1/323581e9273ad2c0dbd1902f3fb50c441da86e894b6e25a73c3fda32c57e/psycopg2_binary-2.9.10-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f8157bed2f51db683f31306aa497311b560f2265998122abe1dce6428bd86567", size = 2959356, upload-time = "2024-10-16T11:22:30.562Z" },
+    { url = "https://files.pythonhosted.org/packages/08/50/d13ea0a054189ae1bc21af1d85b6f8bb9bbc5572991055d70ad9006fe2d6/psycopg2_binary-2.9.10-cp313-cp313-win_amd64.whl", hash = "sha256:27422aa5f11fbcd9b18da48373eb67081243662f9b46e6fd07c3eb46e4535142", size = 2569224, upload-time = "2025-01-04T20:09:19.234Z" },
+    { url = "https://files.pythonhosted.org/packages/a2/bc/e77648009b6e61af327c607543f65fdf25bcfb4100f5a6f3bdb62ddac03c/psycopg2_binary-2.9.10-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:7a813c8bdbaaaab1f078014b9b0b13f5de757e2b5d9be6403639b298a04d218b", size = 3043437, upload-time = "2024-10-16T11:23:42.946Z" },
+    { url = "https://files.pythonhosted.org/packages/e0/e8/5a12211a1f5b959f3e3ccd342eace60c1f26422f53e06d687821dc268780/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d00924255d7fc916ef66e4bf22f354a940c67179ad3fd7067d7a0a9c84d2fbfc", size = 2851340, upload-time = "2024-10-16T11:23:50.038Z" },
+    { url = "https://files.pythonhosted.org/packages/47/ed/5932b0458a7fc61237b653df050513c8d18a6f4083cc7f90dcef967f7bce/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7559bce4b505762d737172556a4e6ea8a9998ecac1e39b5233465093e8cee697", size = 3080905, upload-time = "2024-10-16T11:23:57.932Z" },
+    { url = "https://files.pythonhosted.org/packages/71/df/8047d85c3d23864aca4613c3be1ea0fe61dbe4e050a89ac189f9dce4403e/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e8b58f0a96e7a1e341fc894f62c1177a7c83febebb5ff9123b579418fdc8a481", size = 3264640, upload-time = "2024-10-16T11:24:06.122Z" },
+    { url = "https://files.pythonhosted.org/packages/f3/de/6157e4ef242920e8f2749f7708d5cc8815414bdd4a27a91996e7cd5c80df/psycopg2_binary-2.9.10-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b269105e59ac96aba877c1707c600ae55711d9dcd3fc4b5012e4af68e30c648", size = 3019812, upload-time = "2024-10-16T11:24:17.025Z" },
+    { url = "https://files.pythonhosted.org/packages/25/f9/0fc49efd2d4d6db3a8d0a3f5749b33a0d3fdd872cad49fbf5bfce1c50027/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:79625966e176dc97ddabc142351e0409e28acf4660b88d1cf6adb876d20c490d", size = 2871933, upload-time = "2024-10-16T11:24:24.858Z" },
+    { url = "https://files.pythonhosted.org/packages/57/bc/2ed1bd182219065692ed458d218d311b0b220b20662d25d913bc4e8d3549/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:8aabf1c1a04584c168984ac678a668094d831f152859d06e055288fa515e4d30", size = 2820990, upload-time = "2024-10-16T11:24:29.571Z" },
+    { url = "https://files.pythonhosted.org/packages/71/2a/43f77a9b8ee0b10e2de784d97ddc099d9fe0d9eec462a006e4d2cc74756d/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:19721ac03892001ee8fdd11507e6a2e01f4e37014def96379411ca99d78aeb2c", size = 2919352, upload-time = "2024-10-16T11:24:36.906Z" },
+    { url = "https://files.pythonhosted.org/packages/57/86/d2943df70469e6afab3b5b8e1367fccc61891f46de436b24ddee6f2c8404/psycopg2_binary-2.9.10-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:7f5d859928e635fa3ce3477704acee0f667b3a3d3e4bb109f2b18d4005f38287", size = 2957614, upload-time = "2024-10-16T11:24:44.423Z" },
+    { url = "https://files.pythonhosted.org/packages/85/21/195d69371330983aa16139e60ba855d0a18164c9295f3a3696be41bbcd54/psycopg2_binary-2.9.10-cp39-cp39-win32.whl", hash = "sha256:3216ccf953b3f267691c90c6fe742e45d890d8272326b4a8b20850a03d05b7b8", size = 1025341, upload-time = "2024-10-16T11:24:48.056Z" },
+    { url = "https://files.pythonhosted.org/packages/ad/53/73196ebc19d6fbfc22427b982fbc98698b7b9c361e5e7707e3a3247cf06d/psycopg2_binary-2.9.10-cp39-cp39-win_amd64.whl", hash = "sha256:30e34c4e97964805f715206c7b789d54a78b70f3ff19fbe590104b71c45600e5", size = 1163958, upload-time = "2024-10-16T11:24:51.882Z" },
+]
+
+[[package]]
+name = "pytest"
+version = "8.3.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "colorama", marker = "sys_platform == 'win32'" },
+    { name = "exceptiongroup", marker = "python_full_version < '3.11'" },
+    { name = "iniconfig" },
+    { name = "packaging" },
+    { name = "pluggy" },
+    { name = "tomli", marker = "python_full_version < '3.11'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891, upload-time = "2025-03-02T12:54:54.503Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634, upload-time = "2025-03-02T12:54:52.069Z" },
+]
+
+[[package]]
+name = "pytest-unmagic"
+version = "1.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+    { name = "pytest" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/6e/77/b9c56b38fad9f2b6149bc4f6032f2899f04403f234abe5188b77b18c80e0/pytest_unmagic-1.0.0.tar.gz", hash = "sha256:52e5a6d2394a4feb84654e76f7ac0992ef925f80113de5297b9d1c3f84825fba", size = 10158, upload-time = "2024-10-22T19:10:25.126Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/dc/15/d2ded304f9b62780045f823b8c0b99a9a1612dcfaaa5db4ec13ed566d0d2/pytest_unmagic-1.0.0-py3-none-any.whl", hash = "sha256:4da6eb3c5657ba4772a2c7992fa73e1eb1ad7e2f15defcadde39915be6c02a6f", size = 10754, upload-time = "2024-10-22T19:10:23.374Z" },
+]
+
+[[package]]
+name = "ruff"
+version = "0.11.11"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b2/53/ae4857030d59286924a8bdb30d213d6ff22d8f0957e738d0289990091dd8/ruff-0.11.11.tar.gz", hash = "sha256:7774173cc7c1980e6bf67569ebb7085989a78a103922fb83ef3dfe230cd0687d", size = 4186707, upload-time = "2025-05-22T19:19:34.363Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/b1/14/f2326676197bab099e2a24473158c21656fbf6a207c65f596ae15acb32b9/ruff-0.11.11-py3-none-linux_armv6l.whl", hash = "sha256:9924e5ae54125ed8958a4f7de320dab7380f6e9fa3195e3dc3b137c6842a0092", size = 10229049, upload-time = "2025-05-22T19:18:45.516Z" },
+    { url = "https://files.pythonhosted.org/packages/9a/f3/bff7c92dd66c959e711688b2e0768e486bbca46b2f35ac319bb6cce04447/ruff-0.11.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:c8a93276393d91e952f790148eb226658dd275cddfde96c6ca304873f11d2ae4", size = 11053601, upload-time = "2025-05-22T19:18:49.269Z" },
+    { url = "https://files.pythonhosted.org/packages/e2/38/8e1a3efd0ef9d8259346f986b77de0f62c7a5ff4a76563b6b39b68f793b9/ruff-0.11.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d6e333dbe2e6ae84cdedefa943dfd6434753ad321764fd937eef9d6b62022bcd", size = 10367421, upload-time = "2025-05-22T19:18:51.754Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/50/557ad9dd4fb9d0bf524ec83a090a3932d284d1a8b48b5906b13b72800e5f/ruff-0.11.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7885d9a5e4c77b24e8c88aba8c80be9255fa22ab326019dac2356cff42089fc6", size = 10581980, upload-time = "2025-05-22T19:18:54.011Z" },
+    { url = "https://files.pythonhosted.org/packages/c4/b2/e2ed82d6e2739ece94f1bdbbd1d81b712d3cdaf69f0a1d1f1a116b33f9ad/ruff-0.11.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b5ab797fcc09121ed82e9b12b6f27e34859e4227080a42d090881be888755d4", size = 10089241, upload-time = "2025-05-22T19:18:56.041Z" },
+    { url = "https://files.pythonhosted.org/packages/3d/9f/b4539f037a5302c450d7c695c82f80e98e48d0d667ecc250e6bdeb49b5c3/ruff-0.11.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e231ff3132c1119ece836487a02785f099a43992b95c2f62847d29bace3c75ac", size = 11699398, upload-time = "2025-05-22T19:18:58.248Z" },
+    { url = "https://files.pythonhosted.org/packages/61/fb/32e029d2c0b17df65e6eaa5ce7aea5fbeaed22dddd9fcfbbf5fe37c6e44e/ruff-0.11.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:a97c9babe1d4081037a90289986925726b802d180cca784ac8da2bbbc335f709", size = 12427955, upload-time = "2025-05-22T19:19:00.981Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/e3/160488dbb11f18c8121cfd588e38095ba779ae208292765972f7732bfd95/ruff-0.11.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d8c4ddcbe8a19f59f57fd814b8b117d4fcea9bee7c0492e6cf5fdc22cfa563c8", size = 12069803, upload-time = "2025-05-22T19:19:03.258Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/16/3b006a875f84b3d0bff24bef26b8b3591454903f6f754b3f0a318589dcc3/ruff-0.11.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6224076c344a7694c6fbbb70d4f2a7b730f6d47d2a9dc1e7f9d9bb583faf390b", size = 11242630, upload-time = "2025-05-22T19:19:05.871Z" },
+    { url = "https://files.pythonhosted.org/packages/65/0d/0338bb8ac0b97175c2d533e9c8cdc127166de7eb16d028a43c5ab9e75abd/ruff-0.11.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:882821fcdf7ae8db7a951df1903d9cb032bbe838852e5fc3c2b6c3ab54e39875", size = 11507310, upload-time = "2025-05-22T19:19:08.584Z" },
+    { url = "https://files.pythonhosted.org/packages/6f/bf/d7130eb26174ce9b02348b9f86d5874eafbf9f68e5152e15e8e0a392e4a3/ruff-0.11.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:dcec2d50756463d9df075a26a85a6affbc1b0148873da3997286caf1ce03cae1", size = 10441144, upload-time = "2025-05-22T19:19:13.621Z" },
+    { url = "https://files.pythonhosted.org/packages/b3/f3/4be2453b258c092ff7b1761987cf0749e70ca1340cd1bfb4def08a70e8d8/ruff-0.11.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:99c28505ecbaeb6594701a74e395b187ee083ee26478c1a795d35084d53ebd81", size = 10081987, upload-time = "2025-05-22T19:19:15.821Z" },
+    { url = "https://files.pythonhosted.org/packages/6c/6e/dfa4d2030c5b5c13db158219f2ec67bf333e8a7748dccf34cfa2a6ab9ebc/ruff-0.11.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:9263f9e5aa4ff1dec765e99810f1cc53f0c868c5329b69f13845f699fe74f639", size = 11073922, upload-time = "2025-05-22T19:19:18.104Z" },
+    { url = "https://files.pythonhosted.org/packages/ff/f4/f7b0b0c3d32b593a20ed8010fa2c1a01f2ce91e79dda6119fcc51d26c67b/ruff-0.11.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:64ac6f885e3ecb2fdbb71de2701d4e34526651f1e8503af8fb30d4915a3fe345", size = 11568537, upload-time = "2025-05-22T19:19:20.889Z" },
+    { url = "https://files.pythonhosted.org/packages/d2/46/0e892064d0adc18bcc81deed9aaa9942a27fd2cd9b1b7791111ce468c25f/ruff-0.11.11-py3-none-win32.whl", hash = "sha256:1adcb9a18802268aaa891ffb67b1c94cd70578f126637118e8099b8e4adcf112", size = 10536492, upload-time = "2025-05-22T19:19:23.642Z" },
+    { url = "https://files.pythonhosted.org/packages/1b/d9/232e79459850b9f327e9f1dc9c047a2a38a6f9689e1ec30024841fc4416c/ruff-0.11.11-py3-none-win_amd64.whl", hash = "sha256:748b4bb245f11e91a04a4ff0f96e386711df0a30412b9fe0c74d5bdc0e4a531f", size = 11612562, upload-time = "2025-05-22T19:19:27.013Z" },
+    { url = "https://files.pythonhosted.org/packages/ce/eb/09c132cff3cc30b2e7244191dcce69437352d6d6709c0adf374f3e6f476e/ruff-0.11.11-py3-none-win_arm64.whl", hash = "sha256:6c51f136c0364ab1b774767aa8b86331bd8e9d414e2d107db7a2189f35ea1f7b", size = 10735951, upload-time = "2025-05-22T19:19:30.043Z" },
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.5.3"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999, upload-time = "2024-12-10T12:05:30.728Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415, upload-time = "2024-12-10T12:05:27.824Z" },
+]
+
+[[package]]
+name = "tomli"
+version = "2.2.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" },
+    { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" },
+    { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" },
+    { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" },
+    { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" },
+    { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" },
+    { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" },
+    { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" },
+    { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" },
+    { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" },
+    { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" },
+    { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" },
+    { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" },
+    { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" },
+    { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" },
+    { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" },
+    { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" },
+    { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" },
+    { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" },
+    { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" },
+    { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" },
+    { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" },
+    { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" },
+    { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" },
+    { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" },
+    { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" },
+    { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" },
+    { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" },
+    { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" },
+    { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
+]
+
+[[package]]
+name = "typing-extensions"
+version = "4.13.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967, upload-time = "2025-04-10T14:19:05.416Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806, upload-time = "2025-04-10T14:19:03.967Z" },
+]
+
+[[package]]
+name = "tzdata"
+version = "2025.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" }
+wheels = [
+    { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" },
+]
