Limits and Failure Cases
This page documents situations where RecoverLand does not work, works partially, or may produce an incorrect result. Each case includes an explanation of the cause and a recommendation to avoid it.
Severity scale
| Level | Meaning |
|---|---|
| CRITICAL | Data loss or restore on the wrong feature. The user must understand this risk before using RecoverLand in this context. |
| MODERATE | The system works but with reduced reliability. History remains browsable but automatic restore is not recommended. |
| LOW | Rare edge case. Impact limited to a specific scenario. |
File moved or renamed
MODERATE
The problem
The layer fingerprint uses the absolute path of the source file. If the file is moved, renamed, or copied elsewhere, the new path produces a different fingerprint. The history recorded under the old path is no longer linked to the layer.
What 6.0.0 changed, and what it did not
Version 6.0.0 added an alias mechanism: the datasource_alias table links an obsolete fingerprint to the current one, and every read path expands a fingerprint into all of its historical forms. That mechanism repairs the fingerprints the plugin used to compute incorrectly. It does not repair a file that has actually moved.
The migration recomputes the canonical fingerprint from the source URI recorded in datasource_registry at capture time. For a moved file that URI still holds the old path, so the recomputed value equals the stored one, no alias row is written, and the source is simply counted as already canonical.
Two neighbouring cases are fixed and no longer fork the history of a file:
- A display filter (
Layer > Filter, which QGIS appends as|subset=...) or an opening option (|geometrytype=...). Only the identifying keyslayernameandlayeridtake part in the identity now, so filtering a layer no longer starts a parallel history that a filter-off rewind never sees. - A
spatialiteconnection string or adelimitedtextURI, which used to be passed throughabspathand therefore depended on the directory QGIS was launched from.
Consequence
The existing history remains intact and browsable (it is not deleted). But RecoverLand does not know that the moved file is the same as the old one. Searching by layer does not show the old history, and no action in the dialogs re-attaches it: the alias table is written by the migration, never by the interface. The add_alias function is part of the plugin's public Python API, so an alias can be written by hand from the QGIS console, but that is not a supported workflow: the pair you give it is refused only when it is empty, self-referential, or would close a cycle. Nothing checks that the two fingerprints designate the same data.
How to avoid it
- Do not move data files after you have started editing them in QGIS
- If you must reorganize folders, do it before starting editing work
- Use stable paths (no USB keys, no temporary folders)
Network drive with variable mount point
MODERATE
The problem
By default the fingerprint of a file source contains its absolute path. If a file is accessible as Z:\share\vegetation.shp on one workstation and as \\server\share\vegetation.shp on another, the two paths produce different fingerprints even though they point to the same physical file.
Same problem if the drive letter changes: Z: one day, Y: the next.
Partial mitigation: project-relative fingerprints
The environment variable RECOVERLAND_FINGERPRINT_MODE=relative makes the fingerprint of file-based sources (ogr, spatialite, delimitedtext) carry the path relative to the QGIS project home instead of the absolute path. When the project file and its data live under the same share, the mount point disappears from the fingerprint and the same history is recognised whatever letter the drive was given.
What that mode does not do:
- It is off by default. Any other value, or no value at all, means absolute mode.
- It is an environment variable of the QGIS process, not a project setting. It has to be set on every workstation, and set before capture starts.
- If the project has no home path (a project never saved) or if the data sits on a different drive from the project, no relative form exists: the plugin falls back to the absolute path and writes a WARNING in the log.
- Turning it on later does not re-attach a journal already written under absolute paths. The two forms are recognised as the same layer when RecoverLand looks for the layer an event belongs to, or checks whether a layer is being tracked; the history query is widened only through alias rows, and no alias is created for a mode change.
How to avoid it
- Always use the same path to access the file (always the drive letter, or always the UNC path)
- Fix the network drive letter so it does not change between sessions
- Or keep the project file and its data under one share, and set
RECOVERLAND_FINGERPRINT_MODE=relativeon every workstation before the first capture
Unstable FID (shapefile, GeoJSON)
MODERATE
The problem
The FID of a shapefile is the record number in the .dbf file. This number can change if:
- The shapefile is re-exported ("Save As..." in QGIS or another tool)
- The .dbf file is packed after deletions
- An external script reorganizes the records
What happens now
Until 6.0.0 the consequence was corruption: you recorded a change on feature FID=42 (a building), the shapefile was re-exported, FID 42 became a road, and the restore wrote the building's attributes onto the road while reporting success. That is fixed. Before writing on a target reached by FID alone, the restore has to prove the target is the feature the event describes:
- if a primary key resolved the target, the FID never entered the picture and there is nothing to check;
- if the event captured a post-edit geometry, the feature sitting at that FID must still carry it;
- for an attribute-only edit, the feature sitting at that FID must still carry the new value of every field the event changed.
When none of those can be established, the restore looks for the feature elsewhere in the layer, by the state the event captured after the edit. If that search finds nothing, RecoverLand writes nothing, and says in the log why the action was not applied. Refusing loses a restore; writing would overwrite a feature that is not the one the event describes.
Undoing an INSERT is itself a delete
Undoing an INSERT does not overwrite a feature, it removes one — and on a layer with no primary key that decision rests on a resemblance rather than on an identity. The weakest resemblance the engine still accepts is a single point in common between the feature sitting at the recorded FID and the state the event captured: the geometry, when the event carries one, or one captured attribute still holding its captured value (_occupant_shares_evidence, core/restore_executor.py).
How to avoid it
- Use a format that can carry a real primary key (GeoPackage, SpatiaLite, PostGIS) instead of a shapefile — but see the caveat below: the label RecoverLand puts on a source is not proof that a key exists
- If you must use a shapefile, do not re-export it between captures and restores
- Read the report of a restore, and read it in the log panel (
View > Panels > Log Messages, RecoverLand tab): a refused action names its reason there
"Strong identity" is read off the file extension
refine_ogr_identity in core/support_policy.py decides the identity strength of an OGR source by testing its URI, lowercased, against a series of substrings and returning on the first one that matches: .gpkg, .sqlite, .db and .fgb are called strong, a handful of others medium or weak. The rule worth knowing is the last line of the function rather than the list: a URI that matches nothing in it — any extension the function does not enumerate — is called medium, the same label a shapefile gets. Nothing in that function asks the provider whether a primary key exists.
What actually decides is elsewhere. compute_feature_identity in core/identity.py writes pk_field / pk_value into the recorded identity only when dataProvider().pkAttributeIndexes() is non-empty and the field carries a non-null value on that feature; otherwise the identity is {"fid": N} and nothing else. The restore proof reads that recorded identity, never the strength label.
Recovery horizon without a primary key
CRITICAL
The problem
When a layer's provider declares no primary key, the identity RecoverLand records for a feature is the FID alone: {"fid": 412}. There is nothing else to record.
OGR reuses the FIDs freed by a deletion. Every deletion followed by a compaction puts a different feature under a number that journal entries already refer to. The journal is not wrong — it says what it saw — but the number it holds stops designating the feature it described.
This is not the re-export accident documented above, which happens at one point in time and which you can decide to avoid. It is cumulative and it is silent. Ordinary editing consumes the ability to go back: the further back a rewind reaches, the larger the share of its actions that address FIDs since handed to another feature. Capture keeps working, the journal keeps growing, search keeps returning results. The loss only becomes visible at the moment of the rewind.
What it looks like on a real project
One rewind measured on a real project on 2026-08-12 gave this breakdown:
| Outcome of the 73 planned actions | Counter | Count |
|---|---|---|
| Restored | applied | 6 |
| Already in the target state, nothing to do | skipped_idempotent | 3 |
| Nothing written back: the captured geometry was not valid, and repairing it moved it too far | failed_geometry_drift | 5 |
| Failed: the target could not be established | failed_target_absent | 59 |
Fifty-nine actions out of seventy-three ended with no feature the restore was willing to act on. The events were all present; what the run could not do is establish which feature each of them designates today. Those five numbers — 73, 6, 3, 5, 59 — are the ones pinned in the versioned scenario scripts/validation/scenarios/rw_cycle_summary_truth.py, which replays that breakdown through the real runner. All the repository keeps of that run is that one CYCLE_SUMMARY line of counters: how many layers the rewind covered, and what kind of identity their events carried, are not in it, so this page does not state them. What the scenario checks is that the summary line adds up (the run that produced these counters had announced failed=0 next to 59 failures); it does not re-execute the restore against a layer, so treat the table as one measurement, not as a repeatable benchmark.
failed_geometry_drift has a single producer, _buffer_update in core/restore_executor.py, and it does not mean "the geometry changed since the capture". It fires when the geometry the restore is about to write back is not GEOS-valid: makeValid() repairs it, and when the repaired shape has moved from the original by more than MAKEVALID_DRIFT_TOLERANCE (a value in the layer’s CRS units, core/constants.py) the geometry is not written, rather than putting on the map a shape the user never had. The attributes are written after the geometry, so that action writes nothing at all: the feature is left exactly as the restore found it, and the run counts the action under failed_geometry_drift.
failed_target_absent aggregates three refusals (_classify_restore_result in core/restore_service.py): no feature found at the recorded identity, an occupant that could not be proved to be the right one, and a target the update path declined to verify. FID recycling is the cause this section is about, and it fits the shape of the measured run — but a feature genuinely deleted since capture, a layer re-exported, or a feature whose geometry and captured attributes both changed since the capture, all land in the same bucket. Nothing in the journal separates them. Read 59 as "59 actions found nothing they could prove", not as "59 recycled FIDs".Why RecoverLand cannot repair this for you
The FID is the only identity the layer offers. Rebuilding a lost link would mean guessing which of the current features is the one the event described, and a wrong guess overwrites an unrelated feature — the exact failure this page exists to prevent. So the engine does not rebuild the link: it reports that it could not find the feature.
View > Panels > Log Messages, RecoverLand tab). The CYCLE_SUMMARY line that carries the four counters of the table above goes further still: to the debug file only, <QGIS profile>/recoverland/recoverland_debug.log, with a copy in <project>/.recoverland/recoverland_debug.log. A user who never opens either sees a rewind end with no visible verdict at all.What a primary key changes, and what it does not
When the provider declares a primary key, RecoverLand records it next to the FID (pk_field / pk_value) and resolves the target with it. If the key is not found, the restore stops there and never falls back to the FID, precisely because the FID may point at an unrelated feature. A primary key removes this cause of degradation: the identity travels with the row, and deletions elsewhere in the table cost nothing.
now (UTC) − retention_days. retention_days defaults to 365 and is settable between 7 and 3650 days in the Maintenance dialog, alongside a max_events cap (default 1 000 000) that removes the oldest events beyond that count. Startup auto-purge is off by default, so nothing is deleted until you enable it or run a purge yourself — but once an event is purged it is gone. The time cursor of a rewind cannot be taken back further than the oldest event the journal still holds, so purged history is simply out of reach.How to reduce the exposure
- Give the layer a primary key the provider actually declares: an integer primary key on a GeoPackage or SpatiaLite table, or PostGIS. This is the only measure that removes the cause instead of delaying it. Changing container format without declaring a key changes only the label RecoverLand prints — see "Strong identity" is read off the file extension.
- On a layer with no primary key, treat the journal as a short-term safety net: check and restore soon after the incident, not months later.
- Do not compact or re-export such a layer between a capture and a restore.
- After every rewind, open the Log Messages panel for the counts, and the debug file for the
CYCLE_SUMMARYline behind them.failed_target_absentmeasures how much of that history the engine could no longer tie to a feature; nothing is written for those actions.
FID = -1 (feature being created)
LOW
The problem
During feature creation (before commit), QGIS assigns a temporary FID of -1. If the plugin captures a snapshot at this moment, the recorded identity is {"fid": -1}, which is not unique.
Real impact
This case is rare because RecoverLand captures at commit time (afterCommitChanges), not during editing. At that point, QGIS has already assigned a definitive FID. The risk is theoretical.
Pre-6.0.0 database history left unattached
MODERATE
The problem
Before 6.0.0, the fingerprint of a PostgreSQL, MS SQL or Oracle source captured the schema where it meant the table. Every table of one schema therefore shared a single identity and their histories were merged in the journal.
6.0.0 computes the fingerprint correctly, and the v6 migration attaches the obsolete fingerprints to the new ones by writing alias rows — without rewriting a single journal event. Read paths expand a fingerprint into all of its historical forms, so an old journal stays usable after the upgrade.
That repair is impossible in one case: when an old fingerprint would have to split into several current ones. The journal holds one merged history, several tables claim it, and nothing in the data says which events belong to which table. The migration refuses to guess. It writes no alias row for that source, and the old events stay filed under the merged fingerprint.
How the migration decides
The probe only runs on database providers, and only when the table part of the fingerprint actually moves. It then asks how many distinct QGIS layer identifiers were ever recorded under that fingerprint:
- more than one — the fingerprint covered several layers: ambiguous, no alias;
- exactly one — the fingerprint names one source: the alias is written;
- none recorded at all (journal written by a very old build) — the layer names are used as a fallback, and more than one name is again ambiguous.
The same "leave it alone" outcome applies to a fingerprint the journal has no source address for, and to a pass that could not finish. A pass that did not complete is not marked done: it is retried the next time the project is opened.
Consequence
No event is lost and nothing is rewritten — deleting the alias rows restores the pre-migration state exactly. But a rewind scoped on the current identity of such a table does not reach the events captured under the merged fingerprint.
qlog, which writes to the debug file and to the QGIS Log Messages panel (View > Panels > Log Messages, RecoverLand tab). Nothing appears in the message bar, and nothing appears in the RecoverLand window itself. The warning is emitted once per journal when the RecoverLand dialog refreshes its layer list — not on project opening alone, so a user who never opens the dialog is never told — and again every time a rewind is launched on a degraded journal. If you see that message, treat the older history of the named layers as consultable but not restorable through a rewind on the current identity.Changes outside QGIS
CRITICAL
The problem
RecoverLand listens to QGIS edit signals. If a file is modified by another application (ArcGIS, Python script, text editor, ogr2ogr), RecoverLand detects nothing. These changes are not in the journal.
How to avoid it
Make all your changes in QGIS. If you must use a script, run it in the QGIS Python console in edit mode (startEditing / commitChanges) so that the signals are emitted.
Recording disabled
CRITICAL
If the user disables the recording toggle, no change is captured. RecoverLand cannot recover what was never recorded. There is no catch-up mode.
Disk full or critically low
CRITICAL
The problem
If the disk hosting the journal drops below 100 MB free, RecoverLand automatically disables tracking. Changes made after that point are not captured.
How it is handled
The DiskMonitor module checks disk space when the journal is opened and every 5 minutes after that. When the critical threshold is reached:
- Recording is automatically disabled, and a persistent setting records that the disk monitor is the one who disabled it
- A message giving the volume and the free space left is written to the QGIS Log Messages panel, at ERROR level, and the status bar indicator is refreshed
- At the warning threshold (500 MB), the same channel gets a WARNING-level notice, once; recording continues
- A volume that cannot be measured at all — an unplugged share, a drive letter that disappeared — is treated as critical, not as healthy
Recovery
Free up disk space. The monitor re-enables recording on its own: at the next check — up to 5 minutes later, or immediately if the journal is reopened — if free space is back above the warning threshold of 500 MB and the setting says the disk monitor is the one who disabled recording, tracking is switched back on and a line saying so is written to the log.
tracking_disabled_by_disk marker, so the re-arming branch of the periodic check returns without doing anything and the monitor will not switch recording back on for you (recover.py). In that case the toggle is yours to switch back on. It can still switch it off: the critical branch disables recording whenever the check finds free space below the critical threshold and tracking active, whoever turned it on. What the monitor gives up after your toggle is the re-arming, not the cut.QGIS crash
LOW
The problem
If QGIS crashes during an edit session (before commit), the in-progress changes are not captured because the commit never happened.
If QGIS crashes after the commit but before the WriteQueue has written to SQLite, a few queued events may be lost. SQLite WAL mode protects events already written.
Real impact
The writer thread does not wait for a full batch: it drains the queue as events arrive, writes at most 500 of them per transaction, and when the queue is empty looks again a tenth of a second later. In normal use (a few changes at a time) a committed event reaches SQLite almost at once, so what a crash can lose is what was still queued at that instant.
Recovery mechanism
At startup, the integrity.py module performs several checks:
PRAGMA integrity_checkto detect journal corruption- WAL checkpoint to consolidate pending writes
- Search for a
recoverland_pending.jsonfile containing events not written during the previous crash - Automatic re-integration of these events, and a message when some of them could not be recovered
qlog, like every other RecoverLand message — the QGIS Log Messages panel and the debug file. Despite what the function that emits it is named after, no message bar item and no dialog is raised. Check the panel after a crash rather than assuming silence means nothing was lost.Schema drift
MODERATE
The problem
If a layer's structure changes between capture and restore (field added, removed, renamed, or type changed), the restore may fail or produce an incomplete result.
What RecoverLand does
Before restore, the schema_drift.py module compares the saved schema with the current schema and generates a compatibility report. Missing or incompatible fields are flagged.
Concurrent editing
LOW
RecoverLand is designed for single-workstation use. If two users edit the same file simultaneously, each user has their own journal. A restore by one user may overwrite the other's changes without warning.
This scenario is not supported. For collaborative editing, use a database with a locking system (PostGIS, for example).
Memory layer
LOW
A memory layer (a QGIS scratch layer) has no persistent source file and no stable feature identity. RecoverLand does not capture it at all.
The provider policy in core/support_policy.py gives memory an identity strength of NONE, with capture and restore both off. EditSessionTracker.connect_layer refuses every layer whose strength is NONE and returns before connecting a single edit signal, logging action=refused reason=no_stable_identity. No signal is bound, so no commit is ever seen and no event is ever written.
flog (core/edit_tracker.py), which writes to the debug file recoverland_debug.log and to nothing else. It does not reach the Log Messages panel, so the panel staying empty is not a sign the layer is being captured. Save a scratch layer to a real file before you start doing work in it you would mind losing.Journal size
LOW
The problem
The journal grows indefinitely if retention is not configured. On a project with thousands of daily changes, the file can reach several hundred MB.
What the plugin actually measures
RecoverLand publishes no query-latency measurement, and this page will not invent one. What exists is a classification: evaluate_journal_health in core/health_monitor.py grades the journal on file size and event count and keeps the worse of the two grades.
| Grade | Reached at | What it produces |
|---|---|---|
| healthy | below 50 MB and below 100,000 events | nothing |
| info | 50 MB or 100,000 events | the temporal coverage of the journal, and no suggestion |
| warning | 200 MB or 500,000 events | "journal growing, consider purging", plus a pointer to the Maintenance dialog |
| critical | 500 MB or 1,000,000 events | "large journal, purge recommended" |
These are thresholds someone chose, not the point at which anything was observed to slow down. Crossing 100,000 events changes a label from healthy to info; it does not mean a query became slower there.
Solution
Use the Maintenance dialog to configure a retention policy. For example: keep 90 days of history and purge the rest.
Impact of delta storage
RecoverLand uses delta storage for UPDATE: only the attributes that actually changed are recorded, so changing one field of a fifty-column table stores one value and not fifty. No per-event size is published here, estimated or measured: it depends entirely on how many columns a table has, how many of them an edit touches, and whether the geometry changed. What the plugin does measure is the file itself, through the grades above.
Journal deleted
CRITICAL
If the .recoverland/recoverland_audit.sqlite file is deleted (manually, by a disk cleanup tool, or by an antivirus), the entire history is permanently lost. RecoverLand will create a new empty journal at the next startup.
QGIS project moved
MODERATE
If you move a QGIS project (.qgz) to another folder, the project_fingerprint changes. The existing journal (which is in the .recoverland/ folder at the old location) is no longer found automatically.
How to avoid it
If you move a project, also move the .recoverland/ folder that is next to the .qgz file. The journal will be found if .recoverland is present next to the project file.
Orphan journals (unsaved projects)
LOW
The situation
When a project is never saved, its journal lives in the QGIS profile directory under a hashed filename (audit_<hash>.sqlite). If the project is closed without saving, this journal file becomes an orphan: it exists on disk but is never opened again.
Automatic cleanup
At startup, the JournalManager scans the unsaved-project audit directory and removes orphan journals older than 30 days (excluding the currently active journal). This cleanup is automatic and silent.
What RecoverLand is not
| RecoverLand is not... | Use instead... |
|---|---|
| A versioning system (no branches, no merge) | Git, GeoGig, or a dedicated versioning system |
| A backup system (does not copy files) | Classic file backup, rsync, or a NAS |
| A collaboration system (no sync between workstations) | PostGIS with session management, or a collaborative GIS |
| A real-time monitoring system (no push notifications) | Database triggers or a DBMS-level audit system |
| A Ctrl+Z replacement (only works after commit) | The native QGIS Ctrl+Z for uncommitted changes |
What RecoverLand is
A local safety net. It automatically records every committed change in a SQLite journal, and allows you to find and restore previous states. It protects against common human errors: accidental deletion, incorrect attribute modification, geometry overwrite.
It works best with strong-identity formats (GeoPackage, PostGIS), on a single workstation, with files that remain at the same location.