Add deferrable FK utility, test case, and migration#341
Conversation
rtibblesbot
left a comment
There was a problem hiding this comment.
Solid implementation — checking sqlite_master, calling _remake_table, and no-oping on non-SQLite backends is clean. CI's only failure is the contributor gate, not test failures.
Findings are suggestions and nitpicks. The parameter-naming issue has a real impact on Kolibri callers; the rest are commentary or test hygiene.
- suggestion:
*_app_labelsparams compare againstapp_config.name, not Django's label — silent mismatch for Kolibri callers - suggestion:
_remake_tableprivate-API dependency should be documented - suggestion: mixed immediate/deferrable FK edge case — note as known limitation
- suggestion:
atomic=Trueinrun()vsatomic=Falsein test — worth a clarifying comment - nitpick:
test_iter_apps__allasserts full ordered list;RunPython.noopreverse is the right call
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Ran a phased review pipeline over the pull request diff:
- Classified the diff to select review passes (core, frontend, backend) and whether manual QA was required
- Core review pass checked correctness, design, architecture, testing, completeness, and DRY/SRP/Rule-of-Three principles
- Specialized frontend/backend review passes applied framework-specific lenses where those files changed
- For UI changes: manual QA and an accessibility audit against a live dev server, when available
- Checked CI status and linked issue acceptance criteria
- Synthesized one review from those passes and chose the verdict from the findings, CI status, and QA evidence
20604fa to
4de5f43
Compare
rtibbles
left a comment
There was a problem hiding this comment.
Code and test changes make sense to me.
| # Rebuilding the table from the current model state regenerates the column | ||
| # definitions, which SQLite always emits with the deferrable clause (see | ||
| # DatabaseSchemaEditor.sql_create_inline_fk). Only tested with Django 3.2 | ||
| schema_editor._remake_table(model) |
There was a problem hiding this comment.
I had never heard of this function before this week, now I have reviewed two PRs that contain it!
rtibblesbot
left a comment
There was a problem hiding this comment.
All 6 prior findings resolved. Two new suggestions, one coverage gap.
CI is failing on contributor-gate due to an expired App token — infrastructure, not test failure.
- suggestion (inline):
deferrable_foreign_keys.py:125—DatabaseErrorcaught and not re-raised; withatomic=Falseon the migration, Django still records it as applied even if individual table rebuilds silently fail. If the intent is best-effort, document the contract explicitly and consider logging which tables were skipped so operators can identify them. - suggestion (inline):
test_deferrable_foreign_keys.py:70—test_iter_apps__allhardcodes the fullINSTALLED_APPSlist; any change to test settings breaks this test for unrelated reasons. Prefer[app.name for app in global_apps.get_app_configs()]as the expected value. - suggestion: The three
test_iter_apps__*tests only use apps wherelabel == name('morango','facility_profile'), so theapp_config.labelvsapp_config.namefix has no regression coverage. A one-liner would lock it in:def test_iter_apps__include_by_label(self): # 'auth' label != 'django.contrib.auth' name op = MakeForeignKeysDeferrable(include_app_labels=['auth']) self.assertEqual([app.name for app in op._iter_apps(global_apps)], ['django.contrib.auth'])
Prior-finding status
RESOLVED — morango/deferrable_foreign_keys.py — app_config.name vs app_config.label mismatch
RESOLVED — morango/deferrable_foreign_keys.py — _remake_table private-API documentation
RESOLVED — morango/deferrable_foreign_keys.py — mixed immediate/deferrable FK edge case
RESOLVED — morango/deferrable_foreign_keys.py — atomic=True in run() vs atomic=False in test
WITHDRAWN — tests/testapp/tests/test_deferrable_foreign_keys.py:70 — ordered list assertion (withdrawn prior round)
RESOLVED — tests/testapp/tests/test_deferrable_foreign_keys.py — string-formatting table name without quote_name
@rtibblesbot's comments are generated by an LLM, and should be evaluated accordingly
How was this generated?
Compared the current PR state against findings from a prior review:
- Retrieved prior bot reviews via the GitHub API
- Classified each prior finding as RESOLVED, UNADDRESSED, ACKNOWLEDGED, or CONTESTED
- Only raised NEW findings for newly introduced code
- Ran the same phased review passes as a first review (core, frontend/backend lenses, manual QA when required)
- Synthesized one review from the passes and chose the verdict from the findings, CI status, and QA evidence
| # definitions, which SQLite always emits with the deferrable clause (see | ||
| # DatabaseSchemaEditor.sql_create_inline_fk). Only tested with Django 3.2 | ||
| schema_editor._remake_table(model) | ||
| except DatabaseError as e: |
There was a problem hiding this comment.
suggestion: DatabaseError is caught here but not re-raised. With atomic=False on the migration, there is no wrapping transaction to roll back, so Django will mark the migration as applied even if some table rebuilds silently failed — and the idempotency check at the top means those tables will never be retried. If the intent is best-effort (rebuild as many tables as possible rather than aborting on first failure), document that contract explicitly and emit a summary log at the end listing any tables that failed, so operators can identify and repair them manually. If full atomicity is desired, remove the try/except and let the exception propagate.
|
|
||
| def test_iter_apps__all(self): | ||
| op = MakeForeignKeysDeferrable() | ||
| self.assertEqual([app.name for app in op._iter_apps(global_apps)], [ |
There was a problem hiding this comment.
suggestion: This assertion hardcodes the full ordered INSTALLED_APPS list. Any addition, removal, or reordering of apps in the test settings will break this test for reasons unrelated to _iter_apps. Derive the expected value dynamically instead:
expected = [app.name for app in global_apps.get_app_configs()]
self.assertEqual([app.name for app in op._iter_apps(global_apps)], expected)
Summary
Creation of this utility was started in Kolibri, when it was realized other apps (like Morango) may need this. Since Morango is a standalone package with respect to Kolibri, the utility was added here, made to automatically handle morango models, and to allow Kolibri to leverage the utility in its own way.
Reviewer guidance
I ran Kolibri 0.16.2 to generate an old database with the issue, along with some data (classes and users). With these changes integrated into Kolibri, I then ran both via a Kolibri upgrade on top of the the 0.16.2 home, to verify that it updated the FK constraints. All data was preserved.
I can provide the 0.16.2 home upon request.
Before (0.16.2)
After (pending 0.19.5)
Issues addressed
Supports resolution of learningequality/kolibri#14884
AI Usage
Creation of this utility was started in Kolibri with AI. It generated the bulk of the utility and the test, then it was brought over to Morango, and a human refactored the utility to make it flexible for both Morango and Kolibri, and to isolate looping over applications.