Skip to content

Commit 4840a48

Browse files
committed
Fix condition for creating diff checkpoints
Enfore rule that checkpoint can not contain basefile in its range. When creating diff chain / history, always lookup with version higher than basefile. This prevents confusion when it is worth creating a diff checkpoint and ensures we only create merged diffs which can actually be applied. As a consequence, gpkg will never have checkpoints starting with version 1.
1 parent 4de3a0b commit 4840a48

4 files changed

Lines changed: 35 additions & 16 deletions

File tree

server/mergin/sync/models.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -777,7 +777,10 @@ def diffs_chain(
777777
return None, []
778778

779779
diffs = []
780-
checkpoints = Checkpoint.get_checkpoints(basefile.project_version_name, version)
780+
# get all checkpoints with diffs after basefile which should be applied
781+
checkpoints = Checkpoint.get_checkpoints(
782+
basefile.project_version_name + 1, version
783+
)
781784
expected_diffs = (
782785
FileDiff.query.filter_by(
783786
basefile_id=basefile.id,
@@ -922,6 +925,10 @@ def can_create_checkpoint(file_path_id: int, checkpoint: Checkpoint) -> bool:
922925
if not basefile:
923926
return False
924927

928+
# do not create checkpoint if basefile is present in the range as it does not have valid use case
929+
if basefile.project_version_name >= checkpoint.start:
930+
return False
931+
925932
file_was_deleted = (
926933
FileHistory.query.filter_by(file_path_id=file_path_id)
927934
.filter(
@@ -1017,9 +1024,8 @@ def construct_checkpoint(self) -> bool:
10171024
)
10181025

10191026
for item in cached_items:
1020-
# basefile is a start of the diff chain, item cannot cross over it,
1021-
# but merged diffs can start with basefile version containing changes since then
1022-
if item.start < basefile.project_version_name:
1027+
# basefile is a start of the diff chain, checkpoint must start after basefile version
1028+
if basefile.project_version_name >= item.start:
10231029
continue
10241030

10251031
# find diff in table and on disk

server/mergin/tests/test_file_restore.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,8 @@ def test_version_file_restore(diff_project):
252252
test_file = os.path.join(diff_project.storage.project_dir, "v30", "test.gpkg")
253253
os.rename(test_file, test_file + "_backup")
254254
diff_project.storage.restore_versioned_file("test.gpkg", 30)
255-
checkpoints = Checkpoint.get_checkpoints(9, 30)
255+
# only count diffs from v11-v30 where they were present
256+
checkpoints = Checkpoint.get_checkpoints(11, 30)
256257
assert os.path.exists(test_file)
257258
assert gpkgs_are_equal(test_file, test_file + "_backup")
258259
assert FileDiff.query.filter_by(file_path_id=file_path_id).filter(
@@ -261,6 +262,9 @@ def test_version_file_restore(diff_project):
261262
)
262263
).count() == len(checkpoints)
263264

265+
# checkpoint v9-v12 which contains basefile should not be created
266+
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(1, 3)) is False
267+
264268
# let's create new project with basefile at v1 (which can be start of multiple checkpoints)
265269
working_dir = os.path.join(TMP_DIR, "restore_from_diffs")
266270
basefile = os.path.join(working_dir, "base.gpkg")
@@ -284,10 +288,13 @@ def test_version_file_restore(diff_project):
284288
)
285289
)
286290

291+
# checkpoint v1-v16 which contains basefile should not be created
292+
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(2, 1)) is False
293+
287294
test_file = os.path.join(project.storage.project_dir, "v17", "base.gpkg")
288295
os.rename(test_file, test_file + "_backup")
289296
project.storage.restore_versioned_file("base.gpkg", 17)
290-
checkpoints = Checkpoint.get_checkpoints(1, 17)
297+
checkpoints = Checkpoint.get_checkpoints(2, 17)
291298
assert os.path.exists(test_file)
292299
assert gpkgs_are_equal(test_file, test_file + "_backup")
293300
assert FileDiff.query.filter_by(file_path_id=file_path_id).filter(

server/mergin/tests/test_project_controller.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
)
4141
from ..sync.files import files_changes_from_upload
4242
from ..sync.schemas import ProjectListSchema
43-
from ..sync.utils import generate_checksum, is_versioned_file
43+
from ..sync.utils import Checkpoint, generate_checksum, is_versioned_file
4444
from ..auth.models import User, UserProfile
4545

4646
from . import (
@@ -1915,11 +1915,15 @@ def test_file_diffs_chain(diff_project):
19151915
assert basefile.version.name == 5
19161916
assert len(diffs) == 2
19171917

1918-
# nothing happened in v8 (=v7) but we have now merged diff in chain v5-v8
1918+
# nothing happened in v8 (=v7)
19191919
basefile, diffs = FileHistory.diffs_chain(file_id, 8)
19201920
assert basefile.version.name == 5
1921-
assert len(diffs) == 1
1922-
assert diffs[0].rank == 1 and diffs[0].version == 8
1921+
assert len(diffs) == 2
1922+
assert diffs[0].rank == 0 and diffs[0].version == 6
1923+
assert diffs[1].rank == 0 and diffs[1].version == 7
1924+
1925+
# now merged diff in chain v5-v8 cannot be created as it contains basefile
1926+
assert FileDiff.can_create_checkpoint(file_id, Checkpoint(1, 2)) is False
19231927

19241928
# file was removed in v9
19251929
basefile, diffs = FileHistory.diffs_chain(file_id, 9)

server/mergin/tests/test_public_api_v2.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -303,8 +303,10 @@ def test_create_diff_checkpoint(diff_project):
303303
assert file_diff and os.path.exists(file_diff.abs_path)
304304

305305
basefile, diffs = FileHistory.diffs_chain(file_path_id, 32)
306+
# count checkpoints since we introduced diffs at v11
307+
checkpoints = Checkpoint.get_checkpoints(11, 32)
306308
assert basefile.project_version_name == 9
307-
assert len(diffs) == 3
309+
assert len(diffs) == len(checkpoints)
308310
# also a lower diff rank was created
309311
lower_diff = FileDiff.query.filter_by(version=24, rank=1).first()
310312
assert os.path.exists(lower_diff.abs_path)
@@ -320,8 +322,8 @@ def test_create_diff_checkpoint(diff_project):
320322

321323
basefile, diffs = FileHistory.diffs_chain(file_path_id, 20)
322324
assert basefile.project_version_name == 9
323-
# individual diff v12 + (v13-v16) + (v17-v20) as the last one
324-
assert len(diffs) == 3
325+
# individual diffs v11, v12 + (v13-v16) + (v17-v20) as the last one
326+
assert len(diffs) == 4
325327
assert diffs[-1] == diff
326328

327329
# repeat - nothing to do
@@ -397,9 +399,9 @@ def test_can_create_checkpoint(diff_project):
397399
# for zero rank diffs we can always create a checkpoint (but that should already exist)
398400
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(0, 4)) is True
399401

400-
# there are diffs in both ranges, v1-v4 and v5-v8
401-
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(1, 1)) is True
402-
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(1, 2)) is True
402+
# there are diffs in both ranges, v1-v4 and v5-v8 but both contain basefile
403+
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(1, 1)) is False
404+
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(1, 2)) is False
403405

404406
# higher ranks cannot be created as file was removed at v9
405407
assert FileDiff.can_create_checkpoint(file_path_id, Checkpoint(2, 1)) is False

0 commit comments

Comments
 (0)