-
-
Notifications
You must be signed in to change notification settings - Fork 24
Merge v0.8.x branch into v0.9.x #348
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
rtibbles
merged 26 commits into
learningequality:release-v0.9.x
from
bjester:merge-release-v0.8.x
Jul 29, 2026
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
590c66a
Add regression test for update_fields issue
bjester 00ff230
Implement fix for update_fields issue
bjester 8106e8d
Bump version and update changelog
bjester a571afb
Merge pull request #326 from bjester/pls-save-fields
bjester 83cd4b4
Remove multiprocessing fallback for key generation. Stay in process.
rtibbles ccc64ee
Merge pull request #331 from rtibbles/no_zombies_please_were_british
rtibbles a3fb088
Bump version and update changelog
rtibbles 03bbfc2
Merge pull request #335 from rtibbles/bump_0.8.13
rtibbles a16ebcd
Add deferrable FK utility, test case, and migration
bjester 4de5f43
Bump version and update changelog
bjester 51e70a1
Merge pull request #341 from bjester/make-deferrable-pls
bjester 73bca99
Post 2.17 requests declares urllib3 a normal dep
bjester d3a64a1
Allow repeat push/pull of buffers
bjester 0fd9fa3
Ignore 404s on close/destroy requests due to possible retry
bjester ea045b9
Add retry behavior for low level connection issues not captured by ur…
bjester ddb551f
Update changelog
bjester 3ab3267
Merge pull request #342 from bjester/retry-defense
bjester a390e2b
Improve buffer serialization performance by performing bulk counter l…
bjester 3548306
Bump version and update changelog
bjester 41636c4
Merge pull request #345 from bjester/buffer-serialization-perf
bjester d5f3459
Be more restrictive on what fields are saved during the sync stages
bjester e873adb
Add regression test for handling of is_push
bjester a7d4d66
TransferSession.push is non-nullable boolean, and a False value was i…
bjester f2f513c
Update changelog
bjester 635f8d9
Merge pull request #346 from bjester/tidy-the-strawberry-fields
rtibbles 8f12d91
Resolve merge conflicts
bjester File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,137 @@ | ||
| """ | ||
| Helpers for ensuring that existing SQLite foreign key constraints are | ||
| ``DEFERRABLE INITIALLY DEFERRED``. | ||
|
|
||
| Prior to Django 3.1, SQLite tables were created with immediate foreign key constraints, e.g.:: | ||
|
|
||
| "store_model_id" char(32) NOT NULL REFERENCES "morango_store" ("id") | ||
|
|
||
| Since Django 3.1 the same column is created as:: | ||
|
|
||
| "store_model_id" char(32) NOT NULL REFERENCES "morango_store" ("id") DEFERRABLE INITIALLY DEFERRED | ||
|
|
||
| Django relies on deferred constraint checking for correct cascade-deletion ordering. Databases | ||
| created before Morango 0.8 (with Django 1.11) therefore retain immediate constraints, which can | ||
| raise ``IntegrityError: FOREIGN KEY constraint failed`` during operations such as sync | ||
| deserialization once foreign key enforcement is enabled at the database level (the default since | ||
| Django 3.2). | ||
|
|
||
| The migration framework does not regenerate these constraints on its own because the deferrable | ||
| clause is not part of the field definition that migrations track. This module rebuilds the affected | ||
| tables so that a migrated database ends up with the same schema as a freshly created one. | ||
| """ | ||
|
|
||
| import logging | ||
| from typing import List | ||
| from typing import Optional | ||
|
|
||
| from django.apps import apps as global_apps | ||
| from django.apps.registry import Apps | ||
| from django.db import connection | ||
| from django.db import DatabaseError | ||
| from django.db import router | ||
| from django.db.backends.base.schema import BaseDatabaseSchemaEditor | ||
| from django.db.backends.sqlite3.schema import DatabaseSchemaEditor | ||
|
|
||
| logger = logging.getLogger(__name__) | ||
|
|
||
|
|
||
| def _get_table_sql(conn, table_name: str) -> Optional[str]: | ||
| """ | ||
| Return the ``CREATE TABLE`` statement stored by SQLite for ``table_name``, or ``None`` if the | ||
| table does not exist. | ||
| """ | ||
| with conn.cursor() as cursor: | ||
| cursor.execute( | ||
| "SELECT sql FROM sqlite_master WHERE type = 'table' AND name = %s", | ||
| [table_name], | ||
| ) | ||
| row = cursor.fetchone() | ||
| return row[0] if row else None | ||
|
|
||
|
|
||
| def _table_has_immediate_foreign_key(conn, table_name: str) -> bool: | ||
| """ | ||
| Determine whether ``table_name`` has at least one foreign key constraint that is not deferrable. | ||
| Tables are created with all constraints in the same style, so the presence of a ``REFERENCES`` | ||
| clause without ``DEFERRABLE`` indicates an immediate constraint that needs to be rebuilt. | ||
| """ | ||
| sql = _get_table_sql(conn, table_name) | ||
| if not sql: | ||
| return False | ||
| references_count = sql.count("REFERENCES") | ||
| if references_count == 0: | ||
| return False | ||
| # make sure that all FK columns (identified by references) have a deferrable constraint | ||
| return references_count != sql.count("DEFERRABLE") | ||
|
bjester marked this conversation as resolved.
|
||
|
|
||
|
|
||
| class MakeForeignKeysDeferrable: | ||
| """ | ||
| Invokable utility class that rebuilds of any table belonging to one of ``*app_labels`` whose | ||
| foreign key constraints are still immediate, so that they become | ||
| ``DEFERRABLE INITIALLY DEFERRED``. | ||
|
|
||
| This can be utilized by passing this class to ``RunPython`` in a Django migration, or by | ||
| manually invoking its ``run`` method. | ||
|
|
||
| This is a no-op on non-SQLite backends (PostgreSQL already creates deferrable | ||
| constraints) and on tables that already use deferrable constraints, so it is | ||
| safe to apply to any database. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| include_app_labels: Optional[List[str]] = None, | ||
| exclude_app_labels: Optional[List[str]] = None, | ||
| ): | ||
| self.include_app_labels = include_app_labels | ||
| self.exclude_app_labels = exclude_app_labels | ||
|
|
||
| def _iter_apps(self, apps: Apps): | ||
| for app_config in apps.get_app_configs(): | ||
| if self.exclude_app_labels is not None and app_config.label in self.exclude_app_labels: | ||
| continue | ||
| if self.include_app_labels is None or app_config.label in self.include_app_labels: | ||
| yield app_config | ||
|
|
||
| def __call__(self, apps: Apps, schema_editor: BaseDatabaseSchemaEditor): | ||
| """ | ||
| Loops through all relevant django apps and their models, checking if the model's table has | ||
| FKs, and if so, runs the schema editor's utility that synchronizes the schema to the model, | ||
| which will make FKs deferrable | ||
| """ | ||
| conn = schema_editor.connection | ||
| # these should be in sync, but since we call `_remake_table`, specific to this schema editor | ||
| # class, we ensure we have the correct instance to begin with | ||
| if conn.vendor != "sqlite" or not isinstance(schema_editor, DatabaseSchemaEditor): | ||
| return | ||
|
|
||
| for app_config in self._iter_apps(apps): | ||
| for model in app_config.get_models(include_auto_created=True): | ||
| if not router.allow_migrate_model(conn.alias, model): | ||
| continue | ||
| table_name = model._meta.db_table | ||
| if not _table_has_immediate_foreign_key(conn, table_name): | ||
| continue | ||
| logger.info( | ||
| "Rebuilding table %s to make foreign key constraints deferrable", | ||
| table_name, | ||
| ) | ||
| try: | ||
| # 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) | ||
| except DatabaseError as e: | ||
|
bjester marked this conversation as resolved.
|
||
| logger.error("Failed to rebuild table %s", table_name) | ||
| logger.exception(e) | ||
|
|
||
| def run(self): | ||
| """ | ||
| Invoke this utility outside of Django migrations, manually passing in Django's app registry | ||
| and schema editor. | ||
| """ | ||
| # since this method is meant for manual invocation, we let the editor create a transaction | ||
| with connection.schema_editor(atomic=True) as schema_editor: | ||
| self.__call__(global_apps, schema_editor) | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,24 @@ | ||
| """ | ||
| Make existing foreign key constraints ``DEFERRABLE INITIALLY DEFERRED``. | ||
|
|
||
| Databases created before Morango 0.8 (with Django 1.11) have immediate SQLite foreign key | ||
| constraints, which can break cascade deletions (e.g. during sync) now that foreign keys are enforced | ||
| at the database level. See https://github.com/learningequality/kolibri/issues/14884. | ||
| """ | ||
|
|
||
| from django.db import migrations | ||
|
|
||
| from morango.deferrable_foreign_keys import MakeForeignKeysDeferrable | ||
|
|
||
|
|
||
| class Migration(migrations.Migration): | ||
| dependencies = [ | ||
| ("morango", "0003_store_deserialization_errors"), | ||
| ] | ||
|
|
||
| operations = [ | ||
| migrations.RunPython( | ||
| MakeForeignKeysDeferrable(include_app_labels=["morango"]), | ||
| migrations.RunPython.noop, | ||
| ), | ||
| ] |
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
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
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
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
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
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.