Repository navigation
[pull] master from django-tenants:master - #87
Merged
Merged
Conversation
`_cursor()` has a dedicated branch for named cursors: a named cursor can only
execute one statement, so `SET search_path` has to run on a separate cursor
before the query does. That branch had no test coverage, and it is the one place
where a mistake reads as working code -- a named cursor resolves its table names
when Postgres runs DECLARE, so a missing `SET search_path` returns another
tenant's rows rather than raising.
The tests assert against `pg_cursors`, Postgres' own view of the cursors open in
the session, so they cannot pass by silently falling back to a client-side
fetch -- which is the regression most worth catching, because it is invisible in
the returned rows.
Verified by mutation:
- making `_cursor()` skip `SET search_path` for named cursors fails three of
the five, returning tenant 2's rows where tenant 1's were asserted
- setting DISABLE_SERVER_SIDE_CURSORS globally fails the guard test with an
empty pg_cursors
Passes on both drivers: Django 6.0.8 / psycopg 3.2.13 and Django 5.2.9 /
psycopg2 2.9.12, against PostgreSQL 17.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…tenant-isolation Add tenant-isolation tests for QuerySet.iterator() (server-side cursors)
Django 6.1rc1 is on PyPI and django-tenants currently fails to import on it.
1. Circular import (postgresql_backend/introspection.py)
Django 6.1 added `from django.db.backends.postgresql.base import psycopg_version`
to its PostgreSQL introspection module. `base` already imports `introspection`, so
the two now form a cycle. Django is unaffected because `base` is always imported
first in normal use; the cycle only breaks for a third party importing
`introspection` first, which we did on line 1:
ImportError: cannot import name 'DatabaseIntrospection' from partially
initialized module 'django.db.backends.postgresql.introspection'
Nothing imported at all, so manage.py could not start. Fixed by importing the
backend's base module first. Pure ordering -- no behaviour change on any version.
2. find(all=True) removed (tests/staticfiles/test_finders.py)
The keyword was renamed rather than moved to a new method: 4.2 takes `all`, 5.2
renamed it to `find_all` while still tolerating `all` via **kwargs, and 6.1 dropped
**kwargs so `all=True` raises TypeError. Since 4.2 is still supported, the spelling
is chosen from django.VERSION. TenantFileSystemFinder does not override find(), so
this is test-only.
3. pyproject.toml: `django>=2.1,<6.2`, plus the 6.1 classifier.
4. CI: `==6.1.*` in the matrix, excluding Python 3.10/3.11 (6.1 requires 3.12+).
The Django install gains `--pre` so a series that has not had its final release yet
resolves to its release candidate. Whether pip falls back to a pre-release on its
own turned out to be pip-version dependent -- pip 26.2.1 resolved `Django==6.1.*`
to 6.1rc1, pip 25.0.1 reported "No matching distribution found" -- and an `==X.Y.*`
specifier still keeps every other series on its final release.
Verified with ./run_tests.sh -- both executors plus the create_tenant and
clone_tenant integration steps -- on Python 3.12 / PostgreSQL 17 / psycopg3:
Django 4.2.30 exit 0 | executors OK 2/2 | unit FAIL/ERROR 0 | clone_tenant 1
Django 5.2.17 exit 0 | executors OK 2/2 | unit FAIL/ERROR 0 | clone_tenant 1
Django 6.0.8 exit 0 | executors OK 2/2 | unit FAIL/ERROR 0 | clone_tenant 1
Django 6.1rc1 exit 0 | executors OK 2/2 | unit FAIL/ERROR 0 | clone_tenant 1
Closes #1246
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Django 4.2 LTS reached end of extended support on 7 April 2026 and is listed
under "Unsupported previous releases" upstream -- it receives no further security
or bug fixes. The oldest supported Django is now 5.2 LTS.
- pyproject.toml: `django>=5.2,<6.2`, and the 4.2 classifier is dropped.
`requires-python` stays at >=3.10, which 5.2 still supports.
- CI: the `==4.2.*` matrix entry and its Python 3.13 exclude are removed,
freeing three jobs per run.
With 4.2 gone, three version shims become dead code and are removed:
- `postgresql_backend/base.py`: the try/except around importing `is_psycopg3`
from `psycopg_any`. That module has existed since 4.2, so the ImportError
branch can no longer be reached.
- `tests/test_tenants.py`: the `< (5, 2)` fork over `_pre_setup` being an
instance method versus a classmethod, plus its now-unused imports.
- `tests/staticfiles/test_finders.py`: the runtime choice between the `all` and
`find_all` keywords added for 6.1 support. 5.2 onwards accepts `find_all`, so
it is now unconditional.
- `docs/files.rst`: the DEFAULT_FILE_STORAGE fallback noted for "django < 4.2".
Verified with ./run_tests.sh -- both executors plus the create_tenant and
clone_tenant integration steps -- on Python 3.12 / PostgreSQL 17 / psycopg3:
Django 5.2.17 exit 0 | 130 tests | executors OK 2/2 | FAIL 0 | clone 1
Django 6.0.8 exit 0 | 130 tests | executors OK 2/2 | FAIL 0 | clone 1
Django 6.1rc1 exit 0 | 130 tests | executors OK 2/2 | FAIL 0 | clone 1
The new pin is enforced, not just declared: installing against 4.2 now fails to
resolve rather than silently working.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Add Django 6.1 support
Drop Django 4.2 support (EOL 7 April 2026)
Fixes #1244. `_cursor()` special-cased psycopg3, never reusing the caller's cursor for `SET search_path` in order to avoid the recursion 340f137 describes. Opening a raw `self.connection.cursor()` instead meant the statement bypassed Django's cursor wrapper -- so on psycopg3 it was invisible to assertNumQueries while on psycopg2 it was counted. That is why test_switching_search_path failed on psycopg2: the assertions had been written against psycopg3's accounting rather than the actual behaviour. Following the approach django-pgschemas takes, a `_setting_search_path` flag makes the re-entry a no-op instead of recursing, so the caller's cursor can be reused on both drivers and the statement is visible on both. The SET logic moves out of `_cursor()` into `_handle_search_path()`, which now branches only on whether a cursor was supplied -- never on the driver. This changes no database traffic. With `log_statement='all'`, master and this branch both send exactly 646 `SET search_path` statements for the same test; only their visibility to the cursor wrapper differs. The updated counts in test_switching_search_path{,_limited_calls} therefore make those assertions more accurate rather than looser -- they now count statements that were always executed. Also included, both from the same comparison: - `rollback()` clears the cached search_path. A session-level `SET` is transactional in PostgreSQL, so aborting the transaction that issued it reverts the search_path; with TENANT_LIMIT_SET_CALLS on, the cache could otherwise outlive a value the database had already discarded. - `_get_cursor_search_paths()` validates every part of the search path rather than only `self.schema_name`, which brings PG_EXTRA_SEARCH_PATHS under the same check before it is interpolated into the SET statement. CI gains a `psycopg-version` axis so the psycopg2 fallback is actually exercised. Verified with ./run_tests.sh -- both executors plus the create_tenant and clone_tenant integration steps -- on Python 3.12 / PostgreSQL 17: Dj 5.2.17 psycopg3 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 Dj 5.2.17 psycopg2 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 Dj 6.0.8 psycopg3 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 Dj 6.0.8 psycopg2 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 Dj 6.1rc1 psycopg3 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 Dj 6.1rc1 psycopg2 exit 0 | 130 tests | exec 2/2 | FAIL 0 | clone 1 An earlier revision of this branch also failed on Django 4.2 + psycopg3 with `9 != 6`, because Django's own compose_sql/mogrify opened a second cursor per statement there and each one triggered a SET. Dropping 4.2 in #1248 removed that case, so no version-conditional assertion is needed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…arch-path Make search_path handling driver-agnostic (fixes psycopg2 test failures)
Minor rather than major despite dropping Django 4.2: previous releases have removed Django versions without a major bump (0f559a3 removed 3.1 and 4.0, f3f72d3 reworked supported versions), and 3.13.0 shipped a Breaking section of its own while staying on a minor. Contents: - Breaking: Django 4.2 support dropped, EOL 7 April 2026 (#1248) - New: Django 6.1 support (#1247, #1246) - Fixes: search_path handling made driver-agnostic, so the psycopg2 fallback has a passing suite and is tested in CI (#1249, #1244) - Tests: tenant-isolation coverage for QuerySet.iterator() (#1245) - Docs: "How it works" links in the README and docs index (#1243) Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Bump version to 3.14.0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to subscribe to this conversation on GitHub.
Already have an account?
Sign in.
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
See Commits and Changes for more details.
Created by
pull[bot] (v2.0.0-alpha.4)
Can you help keep this open source service alive? 💖 Please sponsor : )