Skip to content

v0.9.7: fix diff bug, Django 5.2/6.0 + Python 3.13/3.14 compat, CI/packaging modernization#22

Merged
jpic merged 17 commits intoyourlabs:masterfrom
PurpleToti:v0.9.7
Apr 30, 2026
Merged

v0.9.7: fix diff bug, Django 5.2/6.0 + Python 3.13/3.14 compat, CI/packaging modernization#22
jpic merged 17 commits intoyourlabs:masterfrom
PurpleToti:v0.9.7

Conversation

@PurpleToti
Copy link
Copy Markdown
Contributor

@PurpleToti PurpleToti commented Apr 27, 2026

Summary

This PR brings django-dbdiff up to date for current Python and Django versions, fixes a critical
silent bug, and modernizes CI and packaging.


Bug Fixes

  • Critical — fixture.py:129: always-False not diff condition
    The variable name diff in Fixture.diff() shadowed the imported diff function from utils.
    not diff evaluated against a function object, which is always truthy, so the guard was never
    entered. Consequence: (1) the temp dump file was never deleted on a clean match (file leak),
    (2) assertNoDiff received None back and raised TypeError instead of passing.
    Fixed to not different.

  • Fix assertNoDiff None handling — handle diff() returning None on exact match; open
    fixture in binary mode to silence ijson DeprecationWarning.

  • Fix patch_transaction_test_case for Django 5.2 + MySQL
    Django 5.2 changed _reset_sequences from a @classmethod to a @staticmethod; the old
    detection logic broke and called the replacement with the wrong number of arguments. Also fixes
    MySQL AUTO_INCREMENT reset: or 0 left the counter at its previous value in MySQL 8; changed
    to or 1.

  • Fix psycopg version for Python 3.13/3.14 and Django 6.0
    psycopg2-binary==2.9.9 has no wheels for Python 3.13/3.14 (crashes with
    undefined symbol: _PyInterpreterState_Get). Now uses unpinned psycopg2-binary for
    Django 4.2/5.2 and psycopg[binary] (psycopg3) for Django 6.0.

  • Fix tox matrix: drop py311 from django60 envs
    Django 6.0 requires Python 3.12+; py311-django60 was an invalid combination.


New / Extended Features

  • ignore_pk in ContentTypeTestCase — Django 6.0 creates content types with a different
    PK ordering (migration execution order changed). Using ignore_pk=True matches records by
    field values, making the built-in test stable across all supported Django versions.
    Builds on the ignore_pk option added to Fixture and diff() by
    @marianoramirez353 (commit
    1bef061).

Compatibility

Before After
Python 3.8–3.12 3.10–3.14
Django 3.2, 4.0, 4.1, 4.2, 5.0 4.2, 5.2 LTS, 6.0
  • Drops Python 3.8 (EOL Oct 2024) and 3.9 (EOL Oct 2025)
  • Drops Django 4.0 and 4.1 (EOL)
  • Extends PR #21 (Python 3.12 support by
    @pfouque) to Python 3.13 and 3.14 and Django 5.2 LTS / 6.0

CI / Packaging

  • Update actions/checkout@v1v4, actions/setup-python@v4v5
  • Replace deprecated codecov CLI step with codecov/codecov-action@v4
  • Consolidate CI into a single django.yml workflow with lint, docs, and tests jobs
  • Migrate setup.pypyproject.toml (PEP 517/518, setuptools>=61)
  • Migrate QA linter from flake8ruff; fix all lint errors
  • Add Sphinx docs (docs/conf.py, docs/index.rst with autodoc)
  • Replace dead Travis CI badges with GitHub Actions + updated Codecov badges
  • Remove MAINTENANCE.md (planning artifact, no longer needed)

Summary by CodeRabbit

  • Bug Fixes

    • Fixed diff false-negatives, temporary-file leak, and TypeError on exact fixture matches.
  • New Features

    • Added ignore_pk support for non-deterministic primary keys.
    • Added Sphinx-generated API documentation.
  • Documentation

    • Updated README examples and badges; added docs build/config and API index.
  • Tests

    • Added regression tests for exact-match diffs and ignore-pk behavior.
  • Chores

    • Migrated packaging to pyproject.toml; updated supported Python/Django matrix and modernized CI/tox/coverage workflows.

archTortugax and others added 13 commits April 24, 2026 11:50
- fixture.py:129 fix always-False `not diff` → `not different` (temp file
  leak + TypeError in assertNoDiff on exact match); add regression test
- CI: checkout@v4, setup-python@v5, codecov-action@v4; drop py3.8/3.9,
  add py3.13/3.14; add release.yml for automated PyPI publish on tag push
- tox: add django52/django60, drop EOL django40/41/32; migrate qa env from
  flake8 to ruff; fix [gh-actions] mapping to include qa on py3.12
- Migrate setup.py → pyproject.toml (setuptools>=61); bump version to 0.9.7
- README: fix wrong import in usage example, document ignore_pk, update
  supported version matrix, replace Travis/old Codecov badges

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Rename django.yml → tests.yml (test matrix only, no qa)
- Add lint.yml: runs tox -e qa (ruff) standalone
- Add docs.yml: runs tox -e docs (sphinx-build) standalone
- Add [testenv:docs] to tox.ini with sphinx + Django 4.2
- Add docs/conf.py and docs/index.rst with autodoc for public API
- Remove qa from [gh-actions] mapping (has its own workflow now)
- Update README badge to point to tests.yml

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- pyproject.toml: ignore N818 (DbDiffException rename is breaking API),
  blanket-ignore D/E501/I on generated migration files
- fixture.py: wrap long __init__ signature
- test.py: shorten docstring summary line
- utils.py: extract spec variable to shorten long line; noqa C901 on
  diff() and _diff_by_content() (naturally complex algorithms)
- test_utils.py: wrap long assertion dicts, shorten docstring/comment

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- fixture.py: assertNoDiff now handles diff() returning None (exact match
  case exposed by the not-diff bug fix); open fixture in binary mode for
  ijson to silence DeprecationWarning
- utils.py: patch_transaction_test_case detects whether _reset_sequences
  is a classmethod (Django 5.x) or instance method (Django 4.x) and
  defines the replacement accordingly — fixes TypeError on Django 5.2+

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
  checked the wrong thing. The right signal is whether
  _fixture_setup is a classmethod (meaning it calls
  cls._reset_sequences), not whether _reset_sequences itself
  is one.
… AUTO_INCREMENT reset

Django 5.2 changed _reset_sequences to a @staticmethod (not @classmethod),
so isinstance(_raw, classmethod) was False and the else-branch called
_raw(cls, db_name) passing two args to a one-arg function.

Fix: check isinstance(_raw, (classmethod, staticmethod)) to distinguish
both descriptor types from a plain function; for either, call
_original(db_name) since the descriptor protocol already resolves the
callable correctly. Only plain functions (Django 4.x) need _original(cls, db_name).

Also fix MySQL sequence_reset: `or 0` on empty tables sets AUTO_INCREMENT=0
which MySQL 8 ignores (counter stays at previous value). Use `or 1` instead
to force a proper reset to 1.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Django 6.0 requires Python 3.12+; testing py311 against it was invalid.
Confirmed matrix:
  django42: py310-py312
  django52: py310-py314
  django60: py312-py314

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
psycopg2-binary==2.9.9 has no Python 3.13/3.14 wheels; its .so crashes
with undefined symbol: _PyInterpreterState_Get on those interpreters.

Use unpinned psycopg2-binary for django42/52 (latest wheel supports 3.13),
and switch to psycopg[binary] (psycopg3) for django60, which Django 6.0
prefers and which has full 3.13/3.14 support.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Django 6.0 creates content types in a different PK order than earlier
versions (migration execution order changed). Content type PKs are
meaningless — what matters is app_label+model content — so ignore_pk=True
matches records by field values and makes the test stable across versions.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
All tracked work is now reflected in the code, CI, and CHANGELOG.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 27, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fec0d62b-763c-4a2e-a8ff-1a9aea6bab32

📥 Commits

Reviewing files that changed from the base of the PR and between 1db2fdb and 13a43e0.

📒 Files selected for processing (1)
  • dbdiff/tests/test_fixture.py
🚧 Files skipped from review as they are similar to previous changes (1)
  • dbdiff/tests/test_fixture.py

📝 Walkthrough

Walkthrough

Release 0.9.7: packaging moved to pyproject.toml; CI and tox matrices updated (drop Python 3.8/3.9; add 3.13/3.14; add Django 5.2/6.0); Fixture.diff and TransactionTestCase patching logic fixed/refactored; Sphinx docs added; assorted tests, docstrings, imports, and DB backend normalization updated.

Changes

Cohort / File(s) Summary
CI / Workflows
.github/workflows/ci.yml
Rename workflow to CI; split into lint, docs, tests; upgrade actions/checkout/setup-python; adjust Python matrix (remove 3.8/3.9, add 3.13/3.14); replace codecov CLI with codecov/codecov-action@v6 and set fail_ci_if_error: false.
Packaging & Tooling
pyproject.toml, setup.py, tox.ini
Add pyproject.toml with metadata, deps, pytest plugin entry point and Ruff config; remove setup.py content; update tox.ini to target Python 3.10–3.14 and Django 5.2/6.0, switch QA to Ruff, add docs env and update GH Actions mapping.
Documentation
README.rst, docs/conf.py, docs/index.rst
Update badges and usage example (import TransactionTestCase), document ignore_pk=True, update supported versions, add Sphinx conf.py and index.rst for API docs.
Fixture & Diff Logic
dbdiff/fixture.py, dbdiff/utils.py, dbdiff/sequence.py
Fix Fixture.diff() to return None on exact matches; parse fixtures in binary mode for ijson; ensure tempfile cleanup via try/finally; refactor _reset_sequences patching to be classmethod-aware for Django 5.x; set MySQL AUTO_INCREMENT fallback to 1.
Tests
dbdiff/tests/test_fixture.py, dbdiff/tests/test_mixin.py, dbdiff/tests/*
Add regression test asserting Fixture.diff() returns None; set dbdiff_ignore_pk = True in ContentTypeTestCase; reorder imports and adjust minor test formatting/comments.
Docs / Formatting & Docstrings
dbdiff/apps.py, dbdiff/exceptions.py, dbdiff/test.py, dbdiff/serializers/..., dbdiff/tests/project/settings.py, dbdiff/tests/decimal_test/migrations/*, dbdiff/tests/test_decimal.py, dbdiff/tests/test_plugin.py, dbdiff/tests/test_utils.py
Normalize docstring opening style, remove redundant blank lines, and reorder small import lists across modules and tests.
Settings / DB Backends
dbdiff/tests/project/settings_postgresql.py
Switch PostgreSQL backend from django.db.backends.postgresql_psycopg2 to django.db.backends.postgresql and normalize trailing whitespace.
New Docs Build Config
docs/conf.py, docs/index.rst
Add Sphinx configuration that initializes Django and exposes API via autodoc; include README and module autodoc entries.

Sequence Diagram(s)

sequenceDiagram
    participant Runner as Test Runner
    participant Fixture as Fixture.assertNoDiff()
    participant Diff as Fixture.diff()
    participant FS as Tempfile (dump)
    participant Parser as ijson Parser
    participant DB as Database
    participant Comparator as _diff_by_content

    Runner->>Fixture: call assertNoDiff()
    Fixture->>Diff: call diff()
    Diff->>FS: write DB dump to tempfile
    Diff->>Parser: stream-parse fixture (binary mode)
    Parser->>Comparator: yield fixture items
    Comparator->>DB: query DB state/models
    Comparator-->>Diff: return (different, missing, unexpected) or None
    Diff-->>Fixture: return result (None or tuple)
    alt result is None
        Fixture-->>Runner: return (no assertion)
    else diffs found
        Fixture-->>Runner: raise DiffFound with details
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 I hopped through diffs and tidy docs,
Tempfiles cleaned, and CI unblocks.
Pyproject set, tox matrix grown,
Fixtures fixed — no diffs are shown.
A carrot cheer for 0.9.7! 🥕

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 69.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely summarizes the three main changes: a critical bug fix in the diff function, Django 5.2/6.0 and Python 3.13/3.14 compatibility updates, and CI/packaging modernization.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dbdiff/fixture.py (1)

126-130: ⚠️ Potential issue | 🟡 Minor

Minor: temp dump file is leaked when a diff is found.

dump_path is created via tempfile.mkstemp(...) at the top of the method and only unlinked on the no-diff branch (Line 127). When diff() returns the (unexpected, missing, different) tuple at Line 130, the temp file stays on disk for the lifetime of the process. Over a test run with many failing fixtures this can accumulate.

If the file is intentionally kept for debugging when a diff is found, consider documenting that in the docstring; otherwise unlink it before returning the tuple (or use try/finally around the comparison).

♻️ One way to clean up unconditionally
-        if not unexpected and not missing and not different:
-            os.unlink(dump_path)
-            return None
-
-        return unexpected, missing, different
+        os.unlink(dump_path)
+        if not unexpected and not missing and not different:
+            return None
+        return unexpected, missing, different
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/fixture.py` around lines 126 - 130, The temp file created via
tempfile.mkstemp (dump_path) is only unlinked in the "no-diff" branch causing
leaks when diff() returns (unexpected, missing, different); modify the diff
routine (refer to dump_path, unexpected, missing, different) to always remove
the temporary file before returning — either wrap the comparison logic in a
try/finally that unlinks dump_path in the finally block or explicitly call
os.unlink(dump_path) in both the no-diff and diff-return branches (or document
keeping the file in the function docstring if you intend to preserve it for
debugging).
🧹 Nitpick comments (7)
CHANGELOG (1)

1-1: Consider breaking the 0.9.7 entry into multiple lines for readability.

The new entry packs many distinct changes (bug fix, version support, packaging, CI, release workflow) into one long line, while previous entries are concise single-topic summaries. A bulleted/multi-line format would improve scanability.

📝 Suggested format
-0.9.7 Fix always-False diff condition (temp file leak + assertNoDiff TypeError on exact match); add Python 3.10-3.14 and Django 4.2/5.2/6.0 support; drop Python 3.8/3.9 and Django EOL versions; migrate to pyproject.toml; modernize CI (checkout@v4, setup-python@v5, codecov-action); add automated PyPI release workflow
+0.9.7
+  - Fix always-False diff condition causing temp file leak and assertNoDiff
+    TypeError on exact match
+  - Add Python 3.10-3.14 and Django 4.2/5.2/6.0 support
+  - Drop Python 3.8/3.9 and Django EOL versions
+  - Migrate to pyproject.toml
+  - Modernize CI (checkout@v4, setup-python@v5, codecov-action)
+  - Add automated PyPI release workflow
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@CHANGELOG` at line 1, Split the single-line CHANGELOG entry for version
"0.9.7" into multiple lines (one item per line or a short bulleted list) so each
distinct change is its own readable entry: separate the bugfix about the
always-False diff condition (temp file leak + assertNoDiff TypeError), the added
Python 3.10–3.14 and Django 4.2/5.2/6.0 support, the dropped Python 3.8/3.9 and
EOL Django versions, the migration to pyproject.toml, CI modernizations
(checkout@v4, setup-python@v5, codecov-action), and the automated PyPI release
workflow into distinct lines that match the concise style of prior entries under
"0.9.7".
docs/index.rst (1)

1-16: Optional: consider documenting additional public modules.

dbdiff.serializers.json and dbdiff.apps are part of the public surface (referenced from AppConfig.ready() and used by dumpdata) but aren't included in the API reference. Adding them would make the Sphinx output match the package's actual documented surface. Not blocking.

Also, since ..include:: ../README.rst injects whatever heading hierarchy README defines, double-check the rendered TOC depth is what you want — otherwise wrap the API section with an explicit higher-level heading.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/index.rst` around lines 1 - 16, The API docs omit two public modules;
add automodule directives for dbdiff.serializers.json and dbdiff.apps (e.g., add
".. automodule:: dbdiff.serializers.json\n   :members:" and ".. automodule::
dbdiff.apps\n   :members:") alongside the existing automodule entries so Sphinx
includes those public symbols, and if the README include changes heading depth
wrap the API Reference block with an explicit heading level (e.g., a top-level
"API Reference" underline) to ensure the rendered TOC depth is correct.
dbdiff/tests/test_fixture.py (1)

27-32: Good regression coverage for the not diff shadowing fix.

Test correctly exercises the exact-match path that previously leaked temp files and broke assertNoDiff(). One small thought: it implicitly relies on dbdiff/fixtures/dbdiff_test_group.json containing exactly {id: 1, name: 'initial_name'} — if that fixture is ever edited, this test will silently start exercising a non-exact-match path instead. A brief inline comment naming the expected fixture content (or asserting it) would make the regression intent more durable.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/tests/test_fixture.py` around lines 27 - 32, Update
test_diff_exact_match_returns_none to make the expected fixture content
explicit: either add a short inline comment stating that the test relies on the
fixture file containing {"id": 1, "name": "initial_name"}, or add an assertion
that verifies the fixture's loaded data equals that dict before calling
self.fixture.diff(); reference the test method name
(test_diff_exact_match_returns_none) and the call to self.fixture.diff() so
future edits to the fixture won't silently change the test intent.
dbdiff/utils.py (2)

93-97: Nit: redundant list(...) wrap on Line 97.

expected_list is already materialized as a list(...) at Line 93, so for exp_pk, exp_fields in list(expected_list): is wrapping a list in another list. Iterate expected_list directly.

♻️ Suggested cleanup
-        for exp_pk, exp_fields in list(expected_list):
+        for exp_pk, exp_fields in expected_list:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/utils.py` around lines 93 - 97, The for-loop redundantly wraps
expected_list in list(...) even though expected_list was already constructed as
a list; update the loop that iterates over expected_list (the line using "for
exp_pk, exp_fields in list(expected_list):") to iterate directly over
expected_list ("for exp_pk, exp_fields in expected_list:") to remove the
unnecessary list() allocation and clarify intent.

72-73: Nit: redundant setdefault on Line 73.

Line 72 already runs different.setdefault(model, {}), so the chained .setdefault(model, {}).setdefault(pk, {}) on Line 73 re-creates the per-model dict unnecessarily. You can either drop Line 72 (the chained call covers both) or simplify Line 73 to only set the inner pk key.

♻️ Suggested cleanup
-            different.setdefault(model, {})
-            different.setdefault(model, {}).setdefault(pk, {})
+            different.setdefault(model, {}).setdefault(pk, {})
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/utils.py` around lines 72 - 73, The two setdefault calls on the same
dict are redundant; remove the duplicate by keeping a single initialization and
then set the inner pk key. For example, either delete the first
different.setdefault(model, {}) and keep the chained different.setdefault(model,
{}).setdefault(pk, {}), or change the second line to use the already-initialized
mapping (different[model].setdefault(pk, {})) so you only initialize the
per-model dict once; update the code around the different variable where model
and pk are used.
pyproject.toml (1)

45-46: Restrict package discovery to avoid shipping the test suite in the wheel.

[tool.setuptools.packages.find] with only where = ["."] will pick up every importable package under the repo root, which means dbdiff.tests and dbdiff.tests.project.* (settings, migrations, fixtures) get included in the built distribution. That bloats the wheel and lets downstream users import dbdiff.tests.*, which they shouldn't rely on.

♻️ Proposed fix
 [tool.setuptools.packages.find]
 where = ["."]
+include = ["dbdiff*"]
+exclude = ["dbdiff.tests*"]

Verify the resulting layout once applied (e.g. python -m build and inspect the .whl's top-level namespaces).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pyproject.toml` around lines 45 - 46, The package discovery config
[tool.setuptools.packages.find] currently uses where = ["."] which pulls in test
packages; update that section to restrict discovery by adding an exclude pattern
(e.g. exclude = ["dbdiff.tests*", "tests*", "tests.*"]) or explicitly list
packages to include so dbdiff.tests and related test namespaces are omitted;
modify the same [tool.setuptools.packages.find] block (where = ["."]) to add the
exclude key or replace discovery with an explicit package list, then rebuild
(python -m build) and inspect the produced .whl to verify test packages are no
longer present.
.github/workflows/ci.yml (1)

73-76: Optional: bump codecov/codecov-action to v6.

codecov/codecov-action@v6 is the current major version and brings improved tokenless OIDC behavior on public repos, better retry logic, and other enhancements. Not blocking — @v4 still works — but worth a one‑line bump while you're modernizing the rest of the actions.

♻️ Suggested bump
     - name: Upload coverage
-      uses: codecov/codecov-action@v4
+      uses: codecov/codecov-action@v6
       with:
         fail_ci_if_error: false
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/ci.yml around lines 73 - 76, Update the GitHub Actions
step that uses the Codecov action: locate the step with name "Upload coverage"
that currently references uses: codecov/codecov-action@v4 and change it to
codecov/codecov-action@v6; ensure the existing inputs (e.g., fail_ci_if_error:
false) remain compatible and adjust any input names if CI lints complain after
bumping the action.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dbdiff/serializers/base.py`:
- Line 52: Fix the typo in the docstring in serializers.base: change "Strip
trailing zeros for constitency." to "Strip trailing zeros for consistency." —
update the docstring string literal in the serializers/base.py location where
that sentence appears so the module/function/class docstring reads
"consistency".

In `@dbdiff/utils.py`:
- Around line 162-195: Update the patch_transaction_test_case code and comments
to reflect actual Django behavior and remove small redundancies: clarify in the
comment above new_reset_sequences/_needs_explicit_cls that Django 4.x uses a
plain function/staticmethod variations and that passing cls to the original
_reset_sequences is safe across supported Django versions (mention
TransactionTestCase._reset_sequences and new_reset_sequences), remove the
duplicate .setdefault() chaining (the earlier setdefault already populates the
dict) and eliminate the unnecessary list() wrapping of expected_list (it's
already a list), and either add a simple idempotency guard in
patch_transaction_test_case to avoid reapplying the patch on repeated calls or
add a concise comment documenting the recursion/multiple-patch risk and why
current use (apps.ready()) is safe.

In `@tox.ini`:
- Around line 66-75: The docs tox env (testenv:docs) pins Django to
"Django>=4.2,<5.0" which contradicts the repo's supported matrix (4.2 / 5.2 /
6.0) and can hide 5.2+ compatibility or deprecation issues when running
"sphinx-build -W" from docs/conf.py
(DJANGO_SETTINGS_MODULE=dbdiff.tests.project.settings_sqlite); update the
testenv:docs deps to match the primary supported Django version used across the
matrix (e.g., use Django>=5.2,<6.0 or the same range as your envlist/pyproject
classifiers) so docs builds exercise the same Django surface as CI.
- Around line 37-39: Update dbdiff/tests/project/settings_postgresql.py to
replace the legacy ENGINE value 'django.db.backends.postgresql_psycopg2' with
the modern 'django.db.backends.postgresql' so the ENGINE setting in that file
uses django.db.backends.postgresql (compatible with Django 6.0 and
psycopg[binary]); locate the ENGINE assignment in settings_postgresql.py and
change its string value accordingly.

---

Outside diff comments:
In `@dbdiff/fixture.py`:
- Around line 126-130: The temp file created via tempfile.mkstemp (dump_path) is
only unlinked in the "no-diff" branch causing leaks when diff() returns
(unexpected, missing, different); modify the diff routine (refer to dump_path,
unexpected, missing, different) to always remove the temporary file before
returning — either wrap the comparison logic in a try/finally that unlinks
dump_path in the finally block or explicitly call os.unlink(dump_path) in both
the no-diff and diff-return branches (or document keeping the file in the
function docstring if you intend to preserve it for debugging).

---

Nitpick comments:
In @.github/workflows/ci.yml:
- Around line 73-76: Update the GitHub Actions step that uses the Codecov
action: locate the step with name "Upload coverage" that currently references
uses: codecov/codecov-action@v4 and change it to codecov/codecov-action@v6;
ensure the existing inputs (e.g., fail_ci_if_error: false) remain compatible and
adjust any input names if CI lints complain after bumping the action.

In `@CHANGELOG`:
- Line 1: Split the single-line CHANGELOG entry for version "0.9.7" into
multiple lines (one item per line or a short bulleted list) so each distinct
change is its own readable entry: separate the bugfix about the always-False
diff condition (temp file leak + assertNoDiff TypeError), the added Python
3.10–3.14 and Django 4.2/5.2/6.0 support, the dropped Python 3.8/3.9 and EOL
Django versions, the migration to pyproject.toml, CI modernizations
(checkout@v4, setup-python@v5, codecov-action), and the automated PyPI release
workflow into distinct lines that match the concise style of prior entries under
"0.9.7".

In `@dbdiff/tests/test_fixture.py`:
- Around line 27-32: Update test_diff_exact_match_returns_none to make the
expected fixture content explicit: either add a short inline comment stating
that the test relies on the fixture file containing {"id": 1, "name":
"initial_name"}, or add an assertion that verifies the fixture's loaded data
equals that dict before calling self.fixture.diff(); reference the test method
name (test_diff_exact_match_returns_none) and the call to self.fixture.diff() so
future edits to the fixture won't silently change the test intent.

In `@dbdiff/utils.py`:
- Around line 93-97: The for-loop redundantly wraps expected_list in list(...)
even though expected_list was already constructed as a list; update the loop
that iterates over expected_list (the line using "for exp_pk, exp_fields in
list(expected_list):") to iterate directly over expected_list ("for exp_pk,
exp_fields in expected_list:") to remove the unnecessary list() allocation and
clarify intent.
- Around line 72-73: The two setdefault calls on the same dict are redundant;
remove the duplicate by keeping a single initialization and then set the inner
pk key. For example, either delete the first different.setdefault(model, {}) and
keep the chained different.setdefault(model, {}).setdefault(pk, {}), or change
the second line to use the already-initialized mapping
(different[model].setdefault(pk, {})) so you only initialize the per-model dict
once; update the code around the different variable where model and pk are used.

In `@docs/index.rst`:
- Around line 1-16: The API docs omit two public modules; add automodule
directives for dbdiff.serializers.json and dbdiff.apps (e.g., add "..
automodule:: dbdiff.serializers.json\n   :members:" and ".. automodule::
dbdiff.apps\n   :members:") alongside the existing automodule entries so Sphinx
includes those public symbols, and if the README include changes heading depth
wrap the API Reference block with an explicit heading level (e.g., a top-level
"API Reference" underline) to ensure the rendered TOC depth is correct.

In `@pyproject.toml`:
- Around line 45-46: The package discovery config
[tool.setuptools.packages.find] currently uses where = ["."] which pulls in test
packages; update that section to restrict discovery by adding an exclude pattern
(e.g. exclude = ["dbdiff.tests*", "tests*", "tests.*"]) or explicitly list
packages to include so dbdiff.tests and related test namespaces are omitted;
modify the same [tool.setuptools.packages.find] block (where = ["."]) to add the
exclude key or replace discovery with an explicit package list, then rebuild
(python -m build) and inspect the produced .whl to verify test packages are no
longer present.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 96e534a1-1803-4beb-9405-244a34b86718

📥 Commits

Reviewing files that changed from the base of the PR and between 1bef061 and ab81b3a.

📒 Files selected for processing (25)
  • .github/workflows/ci.yml
  • CHANGELOG
  • README.rst
  • dbdiff/apps.py
  • dbdiff/exceptions.py
  • dbdiff/fixture.py
  • dbdiff/sequence.py
  • dbdiff/serializers/base.py
  • dbdiff/serializers/json.py
  • dbdiff/test.py
  • dbdiff/tests/decimal_test/migrations/0001_initial.py
  • dbdiff/tests/decimal_test/migrations/0002_auto_20160102_0914.py
  • dbdiff/tests/project/settings.py
  • dbdiff/tests/project/settings_postgresql.py
  • dbdiff/tests/test_decimal.py
  • dbdiff/tests/test_fixture.py
  • dbdiff/tests/test_mixin.py
  • dbdiff/tests/test_plugin.py
  • dbdiff/tests/test_utils.py
  • dbdiff/utils.py
  • docs/conf.py
  • docs/index.rst
  • pyproject.toml
  • setup.py
  • tox.ini
💤 Files with no reviewable changes (2)
  • dbdiff/serializers/json.py
  • setup.py

Comment thread dbdiff/serializers/base.py Outdated
Comment thread dbdiff/utils.py
Comment thread tox.ini
Comment thread tox.ini
archTortugax and others added 2 commits April 27, 2026 11:01
- serializers/base.py: fix "constitency" typo → "consistency"
- utils.py: remove redundant setdefault() on line 72; remove unnecessary
  list() wrapping in _diff_by_content(); add idempotency note to
  patch_transaction_test_case docstring
- fixture.py: wrap diff() body in try/finally so dump_path is always
  unlinked, even when a diff is found
- tox.ini: bump docs env Django from >=4.2,<5.0 to >=5.2,<6.0
- settings_postgresql.py: update ENGINE to django.db.backends.postgresql
- ci.yml: bump codecov/codecov-action@v4 → @v6
- CHANGELOG: split 0.9.7 entry into one item per line
- test_fixture.py: add inline comment noting expected fixture content
- docs/index.rst: add automodule directives for serializers.json and apps
- pyproject.toml: exclude test packages from wheel discovery

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
dbdiff/utils.py (1)

68-80: ⚠️ Potential issue | 🟠 Major

Build the field diff lazily and compare key sets too.

This still blows up when result_fields is missing a key (Line 75 raises KeyError), and it can also emit a truthy-but-empty different[model][pk] = {} when the only mismatch is an extra key on the result side. Compute a local field_diff over the union of keys first, then persist it only when non-empty.

Proposed fix
+            missing_value = object()
-            different.setdefault(model, {}).setdefault(pk, {})
-
-            for expected_field, expected_value in expected_fields.items():
-                result_value = result_fields[expected_field]
-
-                if expected_value == result_value:
-                    continue
-
-                different[model][pk][expected_field] = (
-                    expected_value,
-                    result_value
-                )
+            field_diff = {}
+            for field_name in expected_fields.keys() | result_fields.keys():
+                expected_value = expected_fields.get(field_name, missing_value)
+                result_value = result_fields.get(field_name, missing_value)
+                if expected_value == result_value:
+                    continue
+                field_diff[field_name] = (expected_value, result_value)
+
+            if field_diff:
+                different.setdefault(model, {})[pk] = field_diff
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/utils.py` around lines 68 - 80, The code currently indexes
result_fields[expected_field] which can KeyError and also writes an empty
different[model][pk] when only extra keys exist; fix by building a local
field_diff dict for the union of keys: compute keys =
set(expected_fields.keys()) | set(result_fields.keys()), iterate those keys, use
expected_fields.get(k, MISSING) and result_fields.get(k, MISSING) (or check
membership) to compare safely, add mismatches to field_diff, and only assign
different.setdefault(model, {}).setdefault(pk, {}) = field_diff (or set it) if
field_diff is non-empty; update references to result_fields, expected_fields,
different, result, model, pk accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@dbdiff/utils.py`:
- Around line 181-187: The code builds SQL by interpolating table names into
statements (variables: tables, statements, cursor, connection) which can break
on names with quotes and triggers S608; change it to execute a parameterized
query per table instead of formatting strings—iterate over tables and call
cursor.execute("UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME=%s", (t,)) (Django
will convert %s to ? for SQLite) so you avoid direct string interpolation and
satisfy the linter.

---

Outside diff comments:
In `@dbdiff/utils.py`:
- Around line 68-80: The code currently indexes result_fields[expected_field]
which can KeyError and also writes an empty different[model][pk] when only extra
keys exist; fix by building a local field_diff dict for the union of keys:
compute keys = set(expected_fields.keys()) | set(result_fields.keys()), iterate
those keys, use expected_fields.get(k, MISSING) and result_fields.get(k,
MISSING) (or check membership) to compare safely, add mismatches to field_diff,
and only assign different.setdefault(model, {}).setdefault(pk, {}) = field_diff
(or set it) if field_diff is non-empty; update references to result_fields,
expected_fields, different, result, model, pk accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d72c52d9-18c1-4983-a22c-a099937b83be

📥 Commits

Reviewing files that changed from the base of the PR and between ab81b3a and 0470eaf.

📒 Files selected for processing (10)
  • .github/workflows/ci.yml
  • CHANGELOG
  • dbdiff/fixture.py
  • dbdiff/serializers/base.py
  • dbdiff/tests/project/settings_postgresql.py
  • dbdiff/tests/test_fixture.py
  • dbdiff/utils.py
  • docs/index.rst
  • pyproject.toml
  • tox.ini
✅ Files skipped from review due to trivial changes (3)
  • dbdiff/serializers/base.py
  • pyproject.toml
  • CHANGELOG
🚧 Files skipped from review as they are similar to previous changes (6)
  • dbdiff/tests/project/settings_postgresql.py
  • docs/index.rst
  • dbdiff/tests/test_fixture.py
  • dbdiff/fixture.py
  • .github/workflows/ci.yml
  • tox.ini

Comment thread dbdiff/utils.py
Comment on lines 181 to 187
statements = [
"UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='%s';" % t
for t in tables
]

cursor = connection.cursor()

for statement in statements:
cursor.execute(statement)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🌐 Web query:

In Django 4.2, 5.2, and 6.0, does cursor.execute("UPDATE sqlite_sequence SET seq=0 WHERE name=%s", [table]) support bound parameters correctly on SQLite via Django's database cursor wrapper?

💡 Result:

Yes, in Django 4.2, 5.2, and 6.0, cursor.execute("UPDATE sqlite_sequence SET seq=0 WHERE name=%s", [table]) supports bound parameters correctly on SQLite via Django's database cursor wrapper. Django's SQLite backend uses SQLiteCursorWrapper, which transparently converts the "%s" format-style placeholder to SQLite's "?" qmark style using regex replacement: FORMAT_QMARK_REGEX.sub("?", query).replace("%%", "%"). This handles the single positional parameter correctly, passing it to the underlying sqlite3.Cursor.execute. This conversion has been stable since early Django versions and remains unchanged in current master (post-6.0). Official docs confirm using "%s" placeholders with cursor.execute(sql, [params]) for SQLite raw queries. Django's sequence reset support (ticket #31479, merged 2020 in Django 3.1+) generates exactly this SQL with bound parameters for ops.sequence_reset_sql and executes it successfully in tests.

Citations:


Parameterize the sqlite_sequence update instead of formatting table names into SQL.

Although db_table values are project-controlled, string interpolation still breaks on valid names containing ' and triggers Ruff's S608 warning. Django's SQLite backend correctly supports parameter binding with %s placeholders, converting them to ? markers automatically. Use parameterized queries to match Django's own pattern for sequence reset operations.

Proposed fix
-        statements = [
-            "UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME='%s';" % t
-            for t in tables
-        ]
-        cursor = connection.cursor()
-        for statement in statements:
-            cursor.execute(statement)
+        with connection.cursor() as cursor:
+            for table in tables:
+                cursor.execute(
+                    "UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME = %s",
+                    [table],
+                )
🧰 Tools
🪛 Ruff (0.15.12)

[error] 182-182: Possible SQL injection vector through string-based query construction

(S608)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@dbdiff/utils.py` around lines 181 - 187, The code builds SQL by interpolating
table names into statements (variables: tables, statements, cursor, connection)
which can break on names with quotes and triggers S608; change it to execute a
parameterized query per table instead of formatting strings—iterate over tables
and call cursor.execute("UPDATE SQLITE_SEQUENCE SET SEQ=0 WHERE NAME=%s", (t,))
(Django will convert %s to ? for SQLite) so you avoid direct string
interpolation and satisfy the linter.

@pfouque
Copy link
Copy Markdown
Contributor

pfouque commented Apr 28, 2026

Hey @PurpleToti — thanks for the thorough work (the not diff shadowing fix especially is a real find 🙏)
I'm not the maintainer but this PR is really hard to evaluate as-is — it bundles changes with very different purposes and risk levels together. The the Django 5.2/6.0 compat or the bug fix could probably be reviewed and merged in minutes, but the CI/packaging modernization requires much more careful attention. Having them all in one place blocks everything.
Splitting into focused PRs (at minimum: bug fix, Django 5.2/6.0 compat, and CI/packaging) is a core open-source contribution practice — this article explains it well. It lets the easy stuff land quickly while the harder changes get the attention they deserve.

Updated comment formatting for clarity in test case. (line was too long)
Comment thread dbdiff/fixture.py

with open(self.path, 'r') as e, open(dump_path, 'r') as r:
expected, result = json.load(e), json.load(r)
try:
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the new try/finally for here exactly?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To ensure tmp file (dump_path) is always deleted

@jpic
Copy link
Copy Markdown
Member

jpic commented Apr 29, 2026

Generally I agréé with your comment @pfouque but I am not sure we can really split, the commits are splitted and reviewable indepently, but we get the final ci outcome here directly

If you have a project using dbdiff, could you try out this branch and report any problem here? Otherwise i suppose we're Moving forward with this, we can still add changes if we need to later on

@pfouque
Copy link
Copy Markdown
Contributor

pfouque commented Apr 29, 2026

TLDR: You're the boss here! ;)

Unfortunately I don't have project using this directly, I was using it working on django-cities-light (and discovered it was barely used in fact: yourlabs/django-cities-light#288).
In such cases I usually build preliminary PR (Like dependency upgrades, pyproject migration, CI refactoring) in separate branches to mitigate the risk. it's usually not that painful and make the history more friendly in case of trouble.

@jpic jpic merged commit 6a2f91f into yourlabs:master Apr 30, 2026
8 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants