From 51c83537bacf9d6c07b57b7f5817660bd8a41ca8 Mon Sep 17 00:00:00 2001 From: Martin Varga Date: Tue, 31 Mar 2026 15:12:35 +0200 Subject: [PATCH] Fix issue with gpkg reconstruction from merged diffs --- server/mergin/sync/models.py | 5 ++- server/mergin/tests/test_file_restore.py | 47 +++++++++++++++++++++--- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/server/mergin/sync/models.py b/server/mergin/sync/models.py index cc7779d2..1b99735b 100644 --- a/server/mergin/sync/models.py +++ b/server/mergin/sync/models.py @@ -1017,8 +1017,9 @@ def construct_checkpoint(self) -> bool: ) for item in cached_items: - # basefile is a start of the diff chain - if item.start <= basefile.project_version_name: + # basefile is a start of the diff chain, item cannot cross over it, + # but merged diffs can start with basefile version containing changes since then + if item.start < basefile.project_version_name: continue # find diff in table and on disk diff --git a/server/mergin/tests/test_file_restore.py b/server/mergin/tests/test_file_restore.py index 42ee2446..8025d7de 100644 --- a/server/mergin/tests/test_file_restore.py +++ b/server/mergin/tests/test_file_restore.py @@ -4,6 +4,7 @@ import os import pytest import shutil +from sqlalchemy import tuple_ from sqlalchemy.orm.attributes import flag_modified from ..app import db @@ -15,6 +16,7 @@ Project, GeodiffActionHistory, ) +from ..sync.utils import Checkpoint from . import test_project_dir, TMP_DIR from .utils import ( create_project, @@ -250,11 +252,46 @@ def test_version_file_restore(diff_project): test_file = os.path.join(diff_project.storage.project_dir, "v30", "test.gpkg") os.rename(test_file, test_file + "_backup") diff_project.storage.restore_versioned_file("test.gpkg", 30) + checkpoints = Checkpoint.get_checkpoints(9, 30) assert os.path.exists(test_file) assert gpkgs_are_equal(test_file, test_file + "_backup") - assert ( - FileDiff.query.filter_by(file_path_id=file_path_id) - .filter(FileDiff.rank > 0) - .count() - > 0 + assert FileDiff.query.filter_by(file_path_id=file_path_id).filter( + tuple_(FileDiff.rank, FileDiff.version).in_( + [(item.rank, item.end) for item in checkpoints] + ) + ).count() == len(checkpoints) + + # let's create new project with basefile at v1 (which can be start of multiple checkpoints) + working_dir = os.path.join(TMP_DIR, "restore_from_diffs") + basefile = os.path.join(working_dir, "base.gpkg") + project = _prepare_restore_project(working_dir) + file_path_id = ( + ProjectFilePath.query.filter_by(project_id=project.id, path="base.gpkg") + .first() + .id ) + + for i in range(17): + sql = "INSERT INTO simple (geometry, name) VALUES (GeomFromText('POINT(24.5, 38.2)', 4326), 'insert_test')" + execute_query(basefile, sql) + pv_latest = push_change(project, "updated", "base.gpkg", working_dir) + assert project.latest_version == pv_latest.name + assert os.path.exists( + os.path.join( + project.storage.project_dir, + ProjectVersion.to_v_name(pv_latest.name), + "base.gpkg", + ) + ) + + test_file = os.path.join(project.storage.project_dir, "v17", "base.gpkg") + os.rename(test_file, test_file + "_backup") + project.storage.restore_versioned_file("base.gpkg", 17) + checkpoints = Checkpoint.get_checkpoints(1, 17) + assert os.path.exists(test_file) + assert gpkgs_are_equal(test_file, test_file + "_backup") + assert FileDiff.query.filter_by(file_path_id=file_path_id).filter( + tuple_(FileDiff.rank, FileDiff.version).in_( + [(item.rank, item.end) for item in checkpoints] + ) + ).count() == len(checkpoints)