MerginMaps / MerginMaps/mobile
Deletion non-selected feature
@xkello is already working on this.
Since Sep 8, 2026.
- Dominant language
- C++
- Stars
- 388
- Forks
- 87
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 20
Description
Description
Feature form acts on a stale feature ID after fid geodiff remapping - save/delete hits a different user's feature
When a pull rebases a GPKG layer, geodiff renumbers the primary keys of local inserts that have not been pushed yet (GEODIFF debug: mapping <old>-><new>). The app does not react to this in any way: open feature forms keep the FeatureLayerPair snapshot they captured before the sync, and save() / deleteFeature() act on that cached fid.
The fid the user's own feature vacates is immediately occupied by another user's feature that arrived in the same pull. So the save or delete lands on somebody else's feature.
Internal support ticket: https://support.lutraconsulting.co.uk/odoo/helpdesk.ticket/16595
Environment
Production
Application (+ app version, build, operating system)
2026.3.0
Android 17
Steps to reproduce
Two clients on the same Mergin project and GPKG layer: QGIS desktop with the Mergin Maps plugin as user B, the mobile app as user A. Both must start on the same project version. Let M be the current max fid in the layer.
- Phone: turn autosync off. Record point and save it - it gets
fid M+1and stays unpushed. - Plugin: add a point to the same layer, save layer edits, and sync. The server now holds the point from the plugin at
fid M+1 - Phone: Turn autosync back on. Tap the point from (1) to open preview. (
panelStateis"preview", somIsSyncPausedis still false. The form controller now holds a snapshot carryingfid M+1.) - Wait a few seconds until the project gets synced. (While rebasing on the server version, geodiff remaps the fid of the phone point)
- Tap
Edit feature. (This is a state change only - thefeatureLayerPairis not re-read.) - Delete the feature.
Expected: the feature the user selected (phone point) is deleted.
Actual: the plugin point from step 2 (the other user's feature) is deleted. Phone point is still on the map. On the next sync, the deletion propagates to the server, and the plugin point disappears for everyone.
Editing an attribute and saving at step 6 instead of deleting shows the same defect silently: the form values (incl. geometry) are written onto the plugin point.
A bit of extra debugging info for the devs:
Four things combine:
-
Autosync is not paused in the
previewpanel state. The pause is only
set on entering the"form"state
(MMFormController.qml:90) and
cleared when the form stack empties
(MMFormStackController.qml:284).
The preview sheet — where the snapshot is captured, and where a user can sit
indefinitely — never pauses. The poll timer runs every 10 s and triggers a
sync after 60 s of inactivity
(autosynccontroller.cpp:49-59). -
preview→formdoes not re-read the feature. It is a bare state
assignment (MMFormController.qml:171,173),
so the staleFeatureLayerPairis carried into the editable form. -
Nothing invalidates open forms after a rebase.
MerginApi::finishProjectSynconly emitsprojectReloadNeededAfterSyncwhen
the GPKG schema changed, the.qgzchanged, or auth config changed
(merginapi.cpp:3719), and that signal is the only
thing the app reacts to (main.cpp:687). Row renumbering
does not qualify, so no reload, no layer refresh, no form invalidation. -
Write paths trust the cached id.
AttributeController::deleteFeature()→
layer()->deleteFeature( mFeatureLayerPair.feature().id() )
(attributecontroller.cpp:1193)AttributeController::save()→layer()->updateFeature( feat, true )
with the pre-sync snapshot
(attributecontroller.cpp:1270),
which matches byfeat.id().
Related defect found while investigating (not the trigger above)
AutosyncController::checkSyncRequiredAfterAppStateChange
(autosynccontroller.cpp:75-92) emits
projectSyncRequested without checking mIsSyncPaused, unlike
syncLayerChange() (autosynccontroller.cpp:69).
Any background → foreground round trip with ≥60 s since the last sync therefore
starts a full pull+rebase regardless of whether a feature form is open.
This was not reproducible as a trigger for the bug above in testing, but it
defeats the form pause and should be guarded regardless.
Also note the pause only suppresses new triggers — a sync already in flight
when a form opens continues to completion, rebase included.
Suggested fixes
Ordered by how much they actually close:
- Re-resolve before writing. In
AttributeController::save()and
deleteFeature(), re-fetch the feature and verify identity before applying
the change, rather than trusting the cachedfid. Bail out with a visible
error if it no longer matches. Closes the defect wherever it is triggered
from. - Invalidate open forms after a rebase. Have the pull report which layers
were rebased and close / reload any open form or preview bound to them.
geodiff already computes the remap table (we log it) — exposing it would let
the app rewrite cached ids instead of discarding the form. - Pause autosync in the
previewstate too, and guard
checkSyncRequiredAfterAppStateChangewithmIsSyncPaused. Narrows the
window but does not close it on its own — a sync already in flight still gets
through.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Assessment
This issue has not been assessed yet.