Hacktoberfest 2026:メンテナが10月に向けて印を付けた、オープンで初心者向けの issue。 Hacktoberfest の issue を見る

Deserializing a deleted record cascades to child app rows without recording their deletion

オープン
#391 コメント 0 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
12/100
issue の種類
バグ
明瞭さ
説明が足りない
活発さ
活発
技術スタック
django, postgresql, python, sqlite
領域
databases

調査の方向性

The issue body says it is not open for contribution, and it asks design questions rather than naming a fix. Start in Store._deserialize_store_model in morango/models/core.py, where post_delete is muted, and in _handle_deleted_store_foreign_keys in morango/sync/operations.py. Done means the maintainers have decided whether cascaded children are recorded in DeletedModels, which the proposed test sketch would then check.

索引モデルが issue の本文から書いたものです。

説明

❌ This issue is not open for contribution. Visit Contributing guidelines to learn about the contributing process and how to find suitable issues.

Observed behavior

When _deserialize_from_store processes a deleted Store record, it deletes the app row with post_delete muted (Store._deserialize_store_model, morango/models/core.py):

with mute_signals(signals.post_delete):
    klass_model.syncing_objects.filter(id=self.id).delete()

Django then cascade-deletes any app rows that reference the deleted row. Because post_delete is muted, those rows are never added to DeletedModels, or to HardDeletedModels when the parent was hard-deleted. A child record's Store row can then stay live while its app row is gone.

Children whose Store records are dirty in the same deserialization are handled differently depending on the model:

  • Models without a self-referential FK. _handle_deleted_store_foreign_keys (morango/sync/operations.py) finds dirty records whose FK points at a deleted Store record. It adds them to DeletedModels and skips saving them.
  • Models with a self-referential FK. Deletion is only spread when validation raises ObjectDoesNotExist (Store._deserialize_store_model). That only happens when a model's clean_fields loads a related object. When it doesn't, FK validation raises ValidationError instead. The child gets a deserialization error and stays dirty, and the deletion is not recorded.
Results

_deserialize_from_store was run with the parent's Store record deleted and dirty. The test app's Facility was run with clean_dereferences_parent = False, so its clean_fields does not load the parent, like a typical production model.

Child Model App row after In DeletedModels Store record after
Exists locally, Store record clean Facility (self-ref FK) deleted by cascade no live, clean
Exists locally, Store record clean SummaryLog (FK to deleted MyUser) deleted by cascade no live, clean
New, Store record dirty Facility (self-ref FK) none no live, dirty, ValidationError
New, Store record dirty SummaryLog (FK to deleted MyUser) none yes live, dirty (marked deleted by the next serialization)

In the first two rows, the Store and tracking tables say the child exists, but it is gone from the app. This device never sends a deletion for it, so peers keep it.

In the third row, the record fails again on every deserialization, unless errored records are skipped.

Suspected scenario

When a record is deleted locally, Django's cascade sends post_delete for each descendant. So the deletion of a parent and the children that device knows about sync together. Since a device always pulls first, and the deletion flag wins when Store records are merged, existing children should be covered.

The scenario we suspect is a child created on another device after the parent was synced, before the deletion reaches that device:

  1. Device A, device B and server S all have parent R.
  2. On A, R is deleted. On B, a new child C is created under R.
  3. B syncs with S and pushes C. S deserializes C normally.
  4. A syncs with S:
    • Pull: A receives C, a new, dirty record whose parent's app row is gone. For a self-referential model, C fails with ValidationError (row 3 above).
    • Push: S receives R's deletion. Deserializing it cascades and removes C's app row on S without recording it (row 1 above).
  5. B pulls R's deletion, and its C app row is removed the same way.

No device ever records C as deleted. Please confirm whether this sequence is reachable in practice, or whether something in Morango or Kolibri prevents it.

Questions

  1. Should the deletion of cascaded child rows be recorded in DeletedModels, plus HardDeletedModels when the parent is hard-deleted? One option is to collect the syncable descendants with Django's Collector before deleting, or to stop muting post_delete for the cascaded rows.
  2. Should the two paths that spread a deletion to dirty children agree? The bulk path spreads it for any FK to a deleted Store record. The self-referential path spreads it only on ObjectDoesNotExist.

Steps to reproduce

Test sketch using the test app models:

@mock.patch.object(Facility, "clean_dereferences_parent", False)
def test_cascaded_child_not_tracked(self):
    root = Facility(id=uuid.uuid4().hex, name="root")
    root.save(update_dirty_bit_to=False)
    child = Facility(id=uuid.uuid4().hex, name="child", parent_id=root.id)
    child.save(update_dirty_bit_to=False)
    # store records: parent deleted and dirty, child clean
    StoreFactory(id=root.id, model_name="facility", deleted=True, dirty_bit=True, ...)
    StoreFactory(id=child.id, model_name="facility", dirty_bit=False, _self_ref_fk=root.id, ...)

    _deserialize_from_store("facilitydata")

    self.assertFalse(Facility.objects.filter(id=child.id).exists())      # cascaded
    self.assertFalse(DeletedModels.objects.filter(id=child.id).exists()) # not tracked
    self.assertFalse(Store.objects.get(id=child.id).deleted)             # still live

Context

Found while porting deserialization to the streaming architecture (#390, part 2 of #210). The streaming sink's delete write is planned to mirror this legacy behavior, so the answer here decides what part 3 does.

AI usage

I used Claude Code to look into this while working through review feedback on #390:

  • Claude traced the legacy deletion paths and wrote a throwaway test that produced the results table above.
  • I corrected its first explanation of how the scenario happens. Updates to existing children are covered, because devices pull first and the deletion flag wins when Store records are merged.
  • Claude drafted this issue, and I reviewed it before filing.
主要言語
Python
スター
15
フォーク
23
平均マージ
1日 11時間
マージ済み PR(30日)
4

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

learningequality/morango のほかの issue

learningequality/morango の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。