Skip to content

Commit 8446c2c

Browse files
committed
Fix unique constraint violation when updating remap table
Fixes #38 This could happen because we were keeping outdated entries in remap tables of work packages - the tuples (master_fid, wp_fid) would get added as needed, but they were never getting deleting when the corresponding features got deleted. As a result, users could get an error from SQLite about unique constraint violation when inserting to the remap table because the master_fid already existed there from some previous runs.
1 parent f12e2ea commit 8446c2c

3 files changed

Lines changed: 189 additions & 4 deletions

File tree

workpackages/remapping.py

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@ def remap_table_master_to_wp(cursor, table_name, wp_name):
5151
For each row:
5252
- remap row exists for master_fid -> use wp_fid
5353
- remap does not exist for master_fid -> insert (master_fid, 1000000+master_fid)
54+
55+
We also clean up remap table not to include any entries where master_fid
56+
does not exist anymore.
5457
"""
5558
remap_table_escaped = remap_table_name(table_name, wp_name)
5659
_create_remap_table_if_not_exists(cursor, remap_table_escaped)
@@ -67,7 +70,7 @@ def remap_table_master_to_wp(cursor, table_name, wp_name):
6770
for row in cursor.execute(sql):
6871
master_fids_missing.add(row[0])
6972

70-
# 2. insert missing mapped ids
73+
# 2. insert missing mapped ids (after a feature got added in master)
7174
cursor.execute(f"""SELECT max(wp_fid) FROM {remap_table_escaped}""")
7275
new_wp_fid = cursor.fetchone()[0]
7376
if new_wp_fid is None:
@@ -79,7 +82,14 @@ def remap_table_master_to_wp(cursor, table_name, wp_name):
7982
cursor.execute(f"""INSERT INTO {remap_table_escaped} VALUES (?, ?)""", (master_fid, new_wp_fid))
8083
new_wp_fid += 1
8184

82-
# 3. remap master ids to WP ids
85+
# 3. delete redundant master ids (after a feature got deleted in master)
86+
sql = (
87+
f"""DELETE FROM {remap_table_escaped} WHERE master_fid NOT IN """
88+
f""" (SELECT {pkey_column_escaped} FROM {table_name_escaped})"""
89+
)
90+
cursor.execute(sql)
91+
92+
# 4. remap master ids to WP ids
8393
mapping = []
8494
sql = (
8595
f"""SELECT {pkey_column_escaped}, mapped.wp_fid FROM {table_name_escaped} """
@@ -105,6 +115,9 @@ def remap_table_wp_to_master(cursor, table_name, wp_name, new_master_fid):
105115
For each row:
106116
- remap row exists for wp_fid -> use master_fid
107117
- remap does not exist for wp_fid -> insert ([first unused master fid], wp_fid)
118+
119+
We also clean up remap table not to include any entries where wp_fid
120+
does not exist anymore.
108121
"""
109122

110123
remap_table = remap_table_name(table_name, wp_name)
@@ -122,12 +135,19 @@ def remap_table_wp_to_master(cursor, table_name, wp_name, new_master_fid):
122135
for row in cursor.execute(sql):
123136
wp_fids_missing.add(row[0])
124137

125-
# 2. insert missing mapped ids
138+
# 2. insert missing mapped ids (after a feature got added in WP)
126139
for wp_fid in wp_fids_missing:
127140
cursor.execute(f"""INSERT INTO {remap_table} VALUES (?, ?)""", (new_master_fid, wp_fid))
128141
new_master_fid += 1
129142

130-
# 3. remap WP ids to master ids
143+
# 3. delete redundant master ids (after a feature got deleted in WP)
144+
sql = (
145+
f"""DELETE FROM {remap_table} WHERE wp_fid NOT IN """
146+
f""" (SELECT {pkey_column_escaped} FROM {table_name_escaped})"""
147+
)
148+
cursor.execute(sql)
149+
150+
# 4. remap WP ids to master ids
131151
mapping = [] # list of tuples (wp_fid, master_fid)
132152
sql = (
133153
f"""SELECT {pkey_column_escaped}, mapped.master_fid FROM {table_name_escaped} """
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
file: farms.gpkg
2+
3+
work-packages:
4+
- name: Kyle
5+
value: 4
6+
mergin-project: martin/farms-Kyle
7+
8+
- name: Kyle_duplicate
9+
value: 4
10+
mergin-project: martin/farms-Kyle-duplicate
11+
12+
- name: Emma
13+
value:
14+
- 1
15+
- 2
16+
mergin-project: martin/farms-Emma
17+
18+
tables:
19+
- name: farms
20+
method: filter-column
21+
filter-column-name: fid
22+
- name: trees
23+
method: filter-column
24+
filter-column-name: farm_id
25+

workpackages/test/test_basic.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,16 @@ def _assert_row_exists(gpkg_filename, table_name, fid):
6060
assert row[0] == 1, f"Row for fid {fid} is not present but it should be"
6161

6262

63+
def _assert_remap_entries(db_filename, table_name, expected_entries):
64+
"""Asserts that the remap table has exactly the given entries"""
65+
db = sqlite3.connect(db_filename)
66+
c = db.cursor()
67+
actual_entries = set()
68+
for row in c.execute(f"""SELECT master_fid, wp_fid FROM {table_name}"""):
69+
actual_entries.add((row[0], row[1]))
70+
assert actual_entries == expected_entries
71+
72+
6373
def _make_initial_farm_work_packages(config_file):
6474
"""
6575
1. create the initial "farms" dataset
@@ -393,6 +403,136 @@ def test_delete_row_master_wp():
393403
_assert_row_missing(os.path.join(output_dir, "Kyle.gpkg"), "trees", 1000001)
394404

395405

406+
def test_remapping_table_wp_to_master():
407+
"""
408+
Test that the remapping tables get correctly updated when features
409+
are inserted/deleted in WP.
410+
411+
Test following workflow scenario (with running make_work_packages function after each step):
412+
- we have 2 WPs with a same filter condition (Kyle, Kyle_duplicate);
413+
- we are adding a new feature (FID: 1111111) to Kyle;
414+
- we are deleting feature (FID: 1111111) from Kyle;
415+
- we are adding a new feature (FID: 2222222) to Kyle_duplicate;
416+
After each step we test that the remap database has expected content.
417+
"""
418+
config_file = os.path.join(this_dir, "config-farm-duplicate-wp.yml")
419+
tmp_dir_1 = _make_initial_farm_work_packages(config_file)
420+
wp_config = load_config_from_yaml(config_file)
421+
422+
# modify 'Kyle' WP by adding a new 'trees' feature with a FID '1111111'
423+
# and run work packaging
424+
tmp_dir_2 = _prepare_next_run_work_packages(tmp_dir_1)
425+
open_layer_and_create_feature(
426+
os.path.join(tmp_dir_2.name, "input", "Kyle.gpkg"),
427+
"trees",
428+
"POINT(6 16)",
429+
{"tree_species_id": 1, "farm_id": 4},
430+
fid=1111111,
431+
)
432+
make_work_packages(tmp_dir_2.name, wp_config)
433+
434+
_assert_remap_entries(
435+
os.path.join(tmp_dir_2.name, "output", "remap.db"),
436+
"trees_Kyle",
437+
set([(8, 1000000), (9, 1000001), (10, 1111111)]),
438+
)
439+
_assert_remap_entries(
440+
os.path.join(tmp_dir_2.name, "output", "remap.db"),
441+
"trees_Kyle_duplicate",
442+
set([(8, 1000000), (9, 1000001), (10, 1000002)]),
443+
)
444+
445+
# modify 'Kyle' WP by removing 'trees' feature with a FID '1111111'
446+
# run work packaging 2nd time
447+
tmp_dir_3 = _prepare_next_run_work_packages(tmp_dir_2)
448+
open_layer_and_delete_feature(os.path.join(tmp_dir_3.name, "input", "Kyle.gpkg"), "trees", 1111111)
449+
make_work_packages(tmp_dir_3.name, wp_config)
450+
451+
_assert_remap_entries(
452+
os.path.join(tmp_dir_3.name, "output", "remap.db"), "trees_Kyle", set([(8, 1000000), (9, 1000001)])
453+
)
454+
_assert_remap_entries(
455+
os.path.join(tmp_dir_3.name, "output", "remap.db"), "trees_Kyle_duplicate", set([(8, 1000000), (9, 1000001)])
456+
)
457+
458+
# modify 'Kyle_duplicate' WP by adding a new 'trees' feature with FID '2222222'
459+
# run work packaging 3rd time
460+
tmp_dir_4 = _prepare_next_run_work_packages(tmp_dir_3)
461+
open_layer_and_create_feature(
462+
os.path.join(tmp_dir_4.name, "input", "Kyle_duplicate.gpkg"),
463+
"trees",
464+
"POINT(6 16)",
465+
{"tree_species_id": 1, "farm_id": 4},
466+
fid=2222222,
467+
)
468+
make_work_packages(tmp_dir_4.name, wp_config)
469+
470+
_assert_remap_entries(
471+
os.path.join(tmp_dir_4.name, "output", "remap.db"),
472+
"trees_Kyle_duplicate",
473+
set([(8, 1000000), (9, 1000001), (10, 2222222)]),
474+
)
475+
_assert_remap_entries(
476+
os.path.join(tmp_dir_4.name, "output", "remap.db"),
477+
"trees_Kyle",
478+
set([(8, 1000000), (9, 1000001), (10, 1000002)]),
479+
)
480+
481+
482+
def test_remapping_table_master_to_wp():
483+
"""
484+
Test that the remapping tables get correctly updated when features
485+
are inserted/deleted in master.
486+
487+
Test following workflow scenario (with running make_work_packages function after each step):
488+
- we are adding a new feature (FID: 10) to master;
489+
- we are deleting feature (FID: 10) from master;
490+
After each step we test that the remap database has expected content.
491+
"""
492+
config_file = os.path.join(this_dir, "config-farm-duplicate-wp.yml")
493+
tmp_dir_1 = _make_initial_farm_work_packages(config_file)
494+
output_dir = os.path.join(tmp_dir_1.name, "output")
495+
output_files = os.listdir(output_dir)
496+
assert "Emma.gpkg" in output_files
497+
assert "Kyle.gpkg" in output_files
498+
assert "Kyle_duplicate.gpkg" in output_files
499+
assert "master.gpkg" in output_files
500+
501+
_assert_remap_entries(
502+
os.path.join(tmp_dir_1.name, "output", "remap.db"), "trees_Kyle", set([(8, 1000000), (9, 1000001)])
503+
)
504+
505+
tmp_dir_2 = _prepare_next_run_work_packages(tmp_dir_1)
506+
507+
# modify master by adding a new 'trees' feature with a FID 10
508+
open_layer_and_create_feature(
509+
os.path.join(tmp_dir_2.name, "input", "master.gpkg"),
510+
"trees",
511+
"POINT(6 16)",
512+
{"tree_species_id": 1, "farm_id": 4},
513+
fid=10,
514+
)
515+
# run work packaging
516+
wp_config = load_config_from_yaml(config_file)
517+
make_work_packages(tmp_dir_2.name, wp_config)
518+
519+
_assert_remap_entries(
520+
os.path.join(tmp_dir_2.name, "output", "remap.db"),
521+
"trees_Kyle",
522+
set([(8, 1000000), (9, 1000001), (10, 1000002)]),
523+
)
524+
525+
tmp_dir_3 = _prepare_next_run_work_packages(tmp_dir_2)
526+
# modify master by removing 'trees' feature with a FID 10
527+
open_layer_and_delete_feature(os.path.join(tmp_dir_3.name, "input", "master.gpkg"), "trees", 10)
528+
# run work packaging 2nd time
529+
make_work_packages(tmp_dir_3.name, wp_config)
530+
531+
_assert_remap_entries(
532+
os.path.join(tmp_dir_3.name, "output", "remap.db"), "trees_Kyle", set([(8, 1000000), (9, 1000001)])
533+
)
534+
535+
396536
# TODO: more test cases
397537
# - delete_master_update_wp # one row deleted in master while it is updated in WP
398538
# - update_master_delete_wp # one row updated in master while it is deleted in WP

0 commit comments

Comments
 (0)