Repository navigation
Scope filters should list read-write partitions last #398
Description
❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.
Overview
Make the read and write filters of a Scope list the read-write partitions last, so that deserialization in filter order puts shared records before the records that reference them.
Background & Motivation
#392 streams deserialization one partition prefix at a time, in the order of the sync filter. A record whose FK target is in a later partition gets a deserialization error, because the target is not in the app table yet. The legacy deserialization looped model by model across all partitions, so the order of the filter did not matter before 0.9.0.
The contract in #392 is that the caller gives the partitions in a usable order. But callers do not always write the filter. Kolibri passes Scope.read_filter and Scope.write_filter directly as its pull and push filters, and Scope builds them as the read-write partitions, then the read-only or write-only partitions:
| Scope | Kolibri template order | Scope.read_filter order |
|---|---|---|
single-user |
read: allusers-ro, user-ro:U. read-write: user-rw:U |
user-rw:U, allusers-ro, user-ro:U |
full-facility |
read-write: D |
D |
In the single-user scope, the logs are in user-rw:U and the FacilityUser is in user-ro:U. Morango deserializes the logs first, and they fail on a device that does not have the user yet. With #392 installed, two of Kolibri's Morango integration tests failed for this reason:
SingleUserSyncRegressionsTestCase::test_kolibri_issue_8439SingleUserSyncRegressionsTestCase::test_issue_fixed_in_morango_pull_146
Both tests passed after Kolibri reordered the read filter itself, with the read-only partitions first. That fix puts a Morango detail in Kolibri, and every other implementer would need the same fix.
User Story / Use Case
As a developer of an app that syncs with Morango,
I want the filters thatScopebuilds from my scope definition to keep a usable order,
So that I control the deserialization order through my templates, and do not have to reorder Morango's filters.
Description & Expected Outcomes
Scope.read_filterlists the read-only partitions, then the read-write partitions.Scope.write_filterlists the write-only partitions, then the read-write partitions.- Within each template, the partitions keep the order of the template.
- The order the filter must have for deserialization is documented, so implementers know how to order their templates.
Read-write partitions usually hold the device's own records, such as a user's logs. Those records usually reference shared records in the read-only partitions, such as the user, the facility and the classrooms. The reverse is rare. This order is a convention, not a guarantee. A read-only record that references a read-write record still fails, and Morango cannot detect this case, because it knows the dependencies between models but not between partitions.
Value Add & Impact
- Implementers that pass
Scopefilters directly get a usable deserialization order without extra code. - Kolibri does not need to reorder the read filter of the
single-userscope.
Deliverables & Contracts
Scope.read_filterandScope.write_filterlist the read-write partitions last.- The docstrings of
Scopeand of the stream source describe the order contract. - The
0.9.0section ofCHANGELOG.mdrecords the new order.
Acceptance Criteria
- A test builds a
Scopefrom templates with read-only, write-only and read-write partitions, and checks the order of both filters. - A deserialization test with a filter in the
single-usershape deserializes a record before the records that reference it. - Kolibri's Morango integration tests pass without Kolibri reordering the read filter, including
test_kolibri_issue_8439,test_issue_fixed_in_morango_pull_146andtest_single_user_course_syncing. -
make testpasses on SQLite and PostgreSQL.
Technical Pointers & Architecture
- Target Components / Context:
Scope.__init__inmorango/models/certificates.py.MorangoSource.prefix_conditionsinmorango/sync/stream/source.pydocuments the order contract. - Related Patterns:
Filter.__add__keeps the order of both operands and removes duplicates. - Data Model & Schema Considerations: No model fields or migrations change.
TransferSession.filterstoresstr(filter), so new transfer sessions store the partitions in the new order. - Resilience & Failure Modes: The comparisons we found do not depend on order. These are
Filter.is_subset_of, which the server uses to validate a requested filter, andFilter.__eq__, whichmorango/sync/context.pyand Kolibri use. Check for any other code that compares filter strings.
Notes & Tradeoffs
- Dependencies: Related to Streaming deserialization part 3: sink #392. Do this before
0.9.0ships, because0.9.0is the first release in which the filter order matters. - Testing Requirements:
tests/testapp/tests/test_api.pytakes the first partition of aread_filterwith.split()[0]. Its scope has no read-write partitions, so the change does not affect it, but check similar tests. - Tradeoffs & Alternatives Considered:
- Kolibri reorders the filter itself. This works, and the Kolibri integration tests pass with it, but each implementer must know how
Scopecombines the templates. - Morango sorts the partition prefixes. Streaming deserialization part 3: sink #392 tried this before release. It broke
test_single_user_course_syncing, where the classroom partitions sort before the user partitions they reference. - Morango deserializes model by model across all partitions, with one query that combines the prefixes, as the legacy code did. This makes the order irrelevant, but we do not want to combine many prefixes into one query expression.
- Kolibri reorders the filter itself. This works, and the Kolibri integration tests pass with it, but each implementer must know how
- Compatibility & Migrations: Devices on older Morango versions do not depend on the filter order, so mixed-version syncs are not affected.
Metadata
Complexity: Low
Target Branch: release-v0.9.x
Labels: TAG: tech update / debt
AI Usage
Drafted with Claude Code while we investigated Kolibri integration test failures against #392. Claude traced the failures to the order of Scope.read_filter, and tested a Kolibri-side reorder that made the tests pass. I rejected a Morango-side fix that combined the prefixes into one query, and chose to track the Scope change in this issue instead of in #392.

Activity