From d5170c7dae08ee78f7fa4c7e1b556408ac467f94 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 17 Mar 2023 13:31:36 +0100 Subject: [PATCH 01/12] function that adds quotes to schema name if the schema name contains upper case letter --- dbsync.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dbsync.py b/dbsync.py index 51ccedf..35585b3 100644 --- a/dbsync.py +++ b/dbsync.py @@ -34,6 +34,12 @@ class DbSyncError(Exception): pass +def _add_quotes_to_schema_name(schema: str) -> str: + if any(ele.isupper() for ele in schema): + schema = f'"{schema}"' + return schema + + def _tables_list_to_string(tables): return ";".join(tables) From 257ee6036494a39cdf504496f8053422e2eb3c90 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 17 Mar 2023 13:41:03 +0100 Subject: [PATCH 02/12] needs string with " if upper case letters, psycopg2 takes care of adding ' --- dbsync.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dbsync.py b/dbsync.py index 35585b3..58dd76c 100644 --- a/dbsync.py +++ b/dbsync.py @@ -236,6 +236,7 @@ def _set_db_project_comment(conn, schema, project_name, version, project_id=None def _get_db_project_comment(conn, schema): """ Get Mergin Maps project name and its current version in db schema""" cur = conn.cursor() + schema = _add_quotes_to_schema_name(schema) cur.execute("SELECT obj_description(%s::regnamespace, 'pg_namespace')", (schema, )) res = cur.fetchone()[0] try: From 2fc60765a2c951db53cd59657a91969bf5830b3e Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 17 Mar 2023 13:45:52 +0100 Subject: [PATCH 03/12] add workspace --- test/test_basic.py | 1 + 1 file changed, 1 insertion(+) diff --git a/test/test_basic.py b/test/test_basic.py index fe08890..5126067 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -17,6 +17,7 @@ SERVER_URL = os.environ.get('TEST_MERGIN_URL') API_USER = os.environ.get('TEST_API_USERNAME') USER_PWD = os.environ.get('TEST_API_PASSWORD') +WORKSPACE = os.environ.get('TEST_API_WORKSPACE') TMP_DIR = tempfile.gettempdir() TEST_DATA_DIR = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'test_data') From 009eae9ed3b7920b028a8b66a5364ecbcf362c4d Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 17 Mar 2023 13:47:14 +0100 Subject: [PATCH 04/12] parametrize tests, so that they run on two project names - adding problematic project name with upper case letter --- test/test_basic.py | 96 ++++++++++++++++++++++++---------------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/test/test_basic.py b/test/test_basic.py index 5126067..4255a20 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -10,7 +10,7 @@ from mergin import MerginClient, ClientError from dbsync import dbsync_init, dbsync_pull, dbsync_push, dbsync_status, config, DbSyncError, _geodiff_make_copy, \ - _get_db_project_comment, _get_mergin_project, _get_project_id, _validate_local_project_id, config + _get_db_project_comment, _get_mergin_project, _get_project_id, _validate_local_project_id, config, _add_quotes_to_schema_name GEODIFF_EXE = os.environ.get('TEST_GEODIFF_EXE') DB_CONNINFO = os.environ.get('TEST_DB_CONNINFO') @@ -45,8 +45,8 @@ def cleanup(mc, project, dirs): def cleanup_db(conn, schema_base, schema_main): """ Removes test schemas from previous tests """ cur = conn.cursor() - cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(schema_base)) - cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(schema_main)) + cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(_add_quotes_to_schema_name(schema_base))) + cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(_add_quotes_to_schema_name(schema_main))) cur.execute("COMMIT") @@ -57,7 +57,7 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables - (re)create local project working directory and sync directory - configure DB sync and let it do the init (make copies to the database) """ - full_project_name = API_USER + "/" + project_name + full_project_name = WORKSPACE + "/" + project_name project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory sync_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync') # used by dbsync db_schema_main = project_name + '_main' @@ -69,7 +69,7 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables cleanup_db(conn, db_schema_base, db_schema_main) # prepare a new Mergin Maps project - mc.create_project(project_name) + mc.create_project(project_name, namespace=WORKSPACE) mc.download_project(full_project_name, project_dir) shutil.copy(source_gpkg_path, os.path.join(project_dir, 'test_sync.gpkg')) for extra_filepath in extra_init_files: @@ -97,12 +97,12 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables dbsync_init(mc, from_gpkg=True) -def test_init_from_gpkg(mc): - project_name = 'test_init' +@pytest.mark.parametrize("project_name", ['test_init', 'Test_init']) +def test_init_from_gpkg(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') - db_schema_main = project_name + '_main' - db_schema_base = project_name + '_base' + db_schema_main = _add_quotes_to_schema_name(project_name + '_main') + db_schema_base = _add_quotes_to_schema_name(project_name + '_base') init_sync_from_geopackage(mc, project_name, source_gpkg_path) @@ -115,7 +115,7 @@ def test_init_from_gpkg(mc): dbsync_init(mc, from_gpkg=True) cur.execute(f"SELECT count(*) from {db_schema_main}.simple") assert cur.fetchone()[0] == 3 - db_proj_info = _get_db_project_comment(conn, db_schema_base) + db_proj_info = _get_db_project_comment(conn, project_name + '_base') assert db_proj_info["name"] == config.connections[0].mergin_project assert db_proj_info["version"] == 'v1' @@ -137,7 +137,7 @@ def test_init_from_gpkg(mc): dbsync_init(mc, from_gpkg=True) cur.execute(f"SELECT count(*) from {db_schema_main}.simple") assert cur.fetchone()[0] == 3 - db_proj_info = _get_db_project_comment(conn, db_schema_base) + db_proj_info = _get_db_project_comment(conn, project_name + '_base') assert db_proj_info["version"] == 'v1' # let's remove local working dir and download different version from server to mimic versions mismatch @@ -145,14 +145,14 @@ def test_init_from_gpkg(mc): mc.download_project(config.connections[0].mergin_project, config.working_dir, 'v2') # run init again, it should handle local working dir properly (e.g. download correct version) and pass but not sync dbsync_init(mc, from_gpkg=True) - db_proj_info = _get_db_project_comment(conn, db_schema_base) + db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v1' # pull server changes to db to make sure we can sync again dbsync_pull(mc) cur.execute(f"SELECT count(*) from {db_schema_main}.simple") assert cur.fetchone()[0] == 4 - db_proj_info = _get_db_project_comment(conn, db_schema_base) + db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v2' # update some feature from 'modified' db to create mismatch with src geopackage, it should pass but not sync @@ -174,7 +174,7 @@ def test_init_from_gpkg(mc): mc.pull_project(project_dir) gpkg_cur.execute(f"SELECT * FROM simple WHERE fid={fid}") assert gpkg_cur.fetchone()[3] == 100 - db_proj_info = _get_db_project_comment(conn, db_schema_base) + db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v3' # update some feature from 'base' db to create mismatch with src geopackage and modified @@ -196,8 +196,8 @@ def test_init_from_gpkg(mc): assert "There are pending changes in the local directory - that should never happen" in str(err.value) -def test_init_from_gpkg_with_incomplete_dir(mc): - project_name = 'test_init' +@pytest.mark.parametrize("project_name", ['test_init', 'Test_init']) +def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') init_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync', project_name) init_sync_from_geopackage(mc, project_name, source_gpkg_path) @@ -209,15 +209,17 @@ def test_init_from_gpkg_with_incomplete_dir(mc): assert os.listdir(init_project_dir) == ['test_sync.gpkg', '.mergin'] -def test_basic_pull(mc): +@pytest.mark.parametrize("project_name", ['test_sync_pull', 'Test_Sync_Pull']) +def test_basic_pull(mc: MerginClient, project_name: str): """ Test initialization and one pull from Mergin Maps to DB 1. create a Mergin Maps project using py-client with a testing gpkg 2. run init, check that everything is fine 3. make change in gpkg (copy new version), check everything is fine """ + project_name_main = _add_quotes_to_schema_name(project_name + "_main") + project_name_base = _add_quotes_to_schema_name(project_name + "_base") - project_name = 'test_sync_pull' source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -227,7 +229,7 @@ def test_basic_pull(mc): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute("SELECT count(*) from test_sync_pull_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 3 # make change in GPKG and push @@ -239,19 +241,20 @@ def test_basic_pull(mc): # check that a feature has been inserted cur = conn.cursor() - cur.execute("SELECT count(*) from test_sync_pull_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 4 - db_proj_info = _get_db_project_comment(conn, 'test_sync_pull_base') + db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v2' print("---") dbsync_status(mc) -def test_basic_push(mc): +@pytest.mark.parametrize("project_name", ['test_sync_push', 'Test_Sync_Push']) +def test_basic_push(mc: MerginClient, project_name: str): """ Initialize a project and test push of a new row from PostgreSQL to Mergin Maps""" + project_name_main = _add_quotes_to_schema_name(project_name + "_main") - project_name = 'test_sync_push' source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -261,19 +264,19 @@ def test_basic_push(mc): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute("SELECT count(*) from test_sync_push_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 3 # make a change in PostgreSQL cur = conn.cursor() - cur.execute("INSERT INTO test_sync_push_main.simple (name, rating) VALUES ('insert in postgres', 123)") + cur.execute(f"INSERT INTO {project_name_main}.simple (name, rating) VALUES ('insert in postgres', 123)") cur.execute("COMMIT") - cur.execute("SELECT count(*) from test_sync_push_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 4 # push the change from DB to PostgreSQL dbsync_push(mc) - db_proj_info = _get_db_project_comment(conn, 'test_sync_push_base') + db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v2' # pull new version of the project to the work project directory @@ -289,13 +292,14 @@ def test_basic_push(mc): dbsync_status(mc) -def test_basic_both(mc): +@pytest.mark.parametrize("project_name", ['test_sync_both', 'Test_Sync_Both']) +def test_basic_both(mc: MerginClient, project_name: str): """ Initializes a sync project and does both a change in Mergin Maps and in the database, and lets DB sync handle it: changes in PostgreSQL need to be rebased on top of changes in Mergin Maps server. """ + project_name_main = _add_quotes_to_schema_name(project_name + "_main") - project_name = 'test_sync_both' source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -305,7 +309,7 @@ def test_basic_both(mc): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name}_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 3 # make change in GPKG and push @@ -314,17 +318,17 @@ def test_basic_both(mc): # make a change in PostgreSQL cur = conn.cursor() - cur.execute(f"INSERT INTO {project_name}_main.simple (name, rating) VALUES ('insert in postgres', 123)") + cur.execute(f"INSERT INTO {project_name_main}.simple (name, rating) VALUES ('insert in postgres', 123)") cur.execute("COMMIT") - cur.execute(f"SELECT count(*) from {project_name}_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 4 # first pull changes from Mergin Maps to DB (+rebase changes in DB) and then push the changes from DB to Mergin Maps dbsync_pull(mc) - db_proj_info = _get_db_project_comment(conn, 'test_sync_both_base') + db_proj_info = _get_db_project_comment(conn, project_name + '_base') assert db_proj_info["version"] == 'v2' dbsync_push(mc) - db_proj_info = _get_db_project_comment(conn, 'test_sync_both_base') + db_proj_info = _get_db_project_comment(conn, project_name + '_base') assert db_proj_info["version"] == 'v3' # pull new version of the project to the work project directory @@ -338,19 +342,20 @@ def test_basic_both(mc): # check that the insert has been applied to the DB cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name}_main.simple") + cur.execute(f"SELECT count(*) from {project_name_main}.simple") assert cur.fetchone()[0] == 5 print("---") dbsync_status(mc) -def test_init_with_skip(mc): - project_name = 'test_init_skip' +@pytest.mark.parametrize("project_name", ['test_init_skip', 'Test_Init_Skip']) +def test_init_with_skip(mc: MerginClient, project_name: str): + source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base_2tables.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') - db_schema_main = project_name + '_main' - db_schema_base = project_name + '_base' + db_schema_main = _add_quotes_to_schema_name(project_name + '_main') + db_schema_base = _add_quotes_to_schema_name(project_name + '_base') init_sync_from_geopackage(mc, project_name, source_gpkg_path, ["lines"]) @@ -381,8 +386,9 @@ def test_init_with_skip(mc): assert cur.fetchone()[0] == 4 -def test_with_local_changes(mc): - project_name = 'test_local_changes' +@pytest.mark.parametrize("project_name", ['test_local_changes', 'Test_Local_Changes']) +def test_with_local_changes(mc: MerginClient, project_name: str): + source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') extra_files = [os.path.join(TEST_DATA_DIR, f) for f in ["note_1.txt", "note_3.txt", "modified_all.gpkg"]] dbsync_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync', @@ -412,17 +418,17 @@ def test_with_local_changes(mc): assert any(local_changes.values()) is False dbsync_status(mc) +@pytest.mark.parametrize("project_name", ['test_recreated_project_ids', 'Test_Recreated_Project_Ids']) +def test_recreated_project_ids(mc: MerginClient, project_name: str): -def test_recreated_project_ids(mc): - project_name = 'test_recreated_project_ids' source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory - full_project_name = API_USER + "/" + project_name + full_project_name = WORKSPACE + "/" + project_name init_sync_from_geopackage(mc, project_name, source_gpkg_path) # delete remote project mc.delete_project(full_project_name) # recreate project with the same name - mc.create_project(project_name) + mc.create_project(project_name, namespace=WORKSPACE) # comparing project IDs after recreating it with the same name mp = _get_mergin_project(project_dir) local_project_id = _get_project_id(mp) From 9b415dbc1e05467df711b433ad52b2108fe06232 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Fri, 17 Mar 2023 16:00:16 +0100 Subject: [PATCH 05/12] add test info about workspace --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index c9f1638..bd440bc 100644 --- a/README.md +++ b/README.md @@ -140,6 +140,7 @@ To run automatic tests: export TEST_MERGIN_URL= # testing server export TEST_API_USERNAME= export TEST_API_PASSWORD= + export TEST_API_WORKSPACE= pytest-3 test/ From bacdf835e926ea00e53f7fafcae74bd091666c21 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 08:45:36 +0100 Subject: [PATCH 06/12] update test name --- test/test_basic.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/test_basic.py b/test/test_basic.py index 4255a20..bc70728 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -97,7 +97,7 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables dbsync_init(mc, from_gpkg=True) -@pytest.mark.parametrize("project_name", ['test_init', 'Test_init']) +@pytest.mark.parametrize("project_name", ['test_init', 'Test_Init']) def test_init_from_gpkg(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') From 0e17527c759d31c046f42d4d0683e4ec60921d03 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 10:17:19 +0100 Subject: [PATCH 07/12] fix tests to properly escape schema names --- test/test_basic.py | 92 +++++++++++++++++++++++----------------------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/test/test_basic.py b/test/test_basic.py index bc70728..1f5b69b 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -7,6 +7,7 @@ import tempfile import psycopg2 +from psycopg2 import sql from mergin import MerginClient, ClientError from dbsync import dbsync_init, dbsync_pull, dbsync_push, dbsync_status, config, DbSyncError, _geodiff_make_copy, \ @@ -45,8 +46,8 @@ def cleanup(mc, project, dirs): def cleanup_db(conn, schema_base, schema_main): """ Removes test schemas from previous tests """ cur = conn.cursor() - cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(_add_quotes_to_schema_name(schema_base))) - cur.execute("DROP SCHEMA IF EXISTS {} CASCADE".format(_add_quotes_to_schema_name(schema_main))) + cur.execute(sql.SQL("DROP SCHEMA IF EXISTS {} CASCADE").format(sql.Identifier(schema_base))) + cur.execute(sql.SQL("DROP SCHEMA IF EXISTS {} CASCADE").format(sql.Identifier(schema_main))) cur.execute("COMMIT") @@ -96,37 +97,36 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables dbsync_init(mc, from_gpkg=True) - @pytest.mark.parametrize("project_name", ['test_init', 'Test_Init']) def test_init_from_gpkg(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') - db_schema_main = _add_quotes_to_schema_name(project_name + '_main') - db_schema_base = _add_quotes_to_schema_name(project_name + '_base') + db_schema_main = project_name + '_main' + db_schema_base = project_name + '_base' init_sync_from_geopackage(mc, project_name, source_gpkg_path) # test that database schemas are created + tables are populated conn = psycopg2.connect(DB_CONNINFO) cur = conn.cursor() - cur.execute(f"SELECT count(*) from {db_schema_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) assert cur.fetchone()[0] == 3 # run again, nothing should change dbsync_init(mc, from_gpkg=True) - cur.execute(f"SELECT count(*) from {db_schema_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) assert cur.fetchone()[0] == 3 - db_proj_info = _get_db_project_comment(conn, project_name + '_base') + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["name"] == config.connections[0].mergin_project assert db_proj_info["version"] == 'v1' # rename base schema to mimic some mismatch - cur.execute(f"ALTER SCHEMA {db_schema_base} RENAME TO schema_tmp") + cur.execute(sql.SQL("ALTER SCHEMA {} RENAME TO schema_tmp").format(sql.Identifier(db_schema_base)).as_string(conn)) conn.commit() with pytest.raises(DbSyncError) as err: dbsync_init(mc, from_gpkg=True) assert "The 'modified' schema exists but the base schema is missing" in str(err.value) # and revert back - cur.execute(f"ALTER SCHEMA schema_tmp RENAME TO {db_schema_base}") + cur.execute(sql.SQL("ALTER SCHEMA schema_tmp RENAME TO {}").format(sql.Identifier(db_schema_base)).as_string(conn)) conn.commit() # make change in GPKG and push to server to create pending changes, it should pass but not sync @@ -135,9 +135,9 @@ def test_init_from_gpkg(mc: MerginClient, project_name: str): # remove local copy of project (to mimic loss at docker restart) shutil.rmtree(config.working_dir) dbsync_init(mc, from_gpkg=True) - cur.execute(f"SELECT count(*) from {db_schema_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) assert cur.fetchone()[0] == 3 - db_proj_info = _get_db_project_comment(conn, project_name + '_base') + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v1' # let's remove local working dir and download different version from server to mimic versions mismatch @@ -145,23 +145,23 @@ def test_init_from_gpkg(mc: MerginClient, project_name: str): mc.download_project(config.connections[0].mergin_project, config.working_dir, 'v2') # run init again, it should handle local working dir properly (e.g. download correct version) and pass but not sync dbsync_init(mc, from_gpkg=True) - db_proj_info = _get_db_project_comment(conn, project_name + "_base") + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v1' # pull server changes to db to make sure we can sync again dbsync_pull(mc) - cur.execute(f"SELECT count(*) from {db_schema_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) assert cur.fetchone()[0] == 4 - db_proj_info = _get_db_project_comment(conn, project_name + "_base") + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v2' # update some feature from 'modified' db to create mismatch with src geopackage, it should pass but not sync fid = 1 - cur.execute(f"SELECT * from {db_schema_main}.simple WHERE fid={fid}") + cur.execute(sql.SQL("SELECT * from {}.simple WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) old_value = cur.fetchone()[3] - cur.execute(f"UPDATE {db_schema_main}.simple SET rating=100 WHERE fid={fid}") + cur.execute(sql.SQL("UPDATE {}.simple SET rating=100 WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) conn.commit() - cur.execute(f"SELECT * from {db_schema_main}.simple WHERE fid={fid}") + cur.execute(sql.SQL("SELECT * from {}.simple WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) assert cur.fetchone()[3] == 100 dbsync_init(mc, from_gpkg=True) # check geopackage has not been modified - after init we are not synced! @@ -172,18 +172,18 @@ def test_init_from_gpkg(mc: MerginClient, project_name: str): # push db changes to server (and download new version to local working dir) to make sure we can sync again dbsync_push(mc) mc.pull_project(project_dir) - gpkg_cur.execute(f"SELECT * FROM simple WHERE fid={fid}") + gpkg_cur.execute(f"SELECT * FROM simple WHERE fid ={fid}") assert gpkg_cur.fetchone()[3] == 100 - db_proj_info = _get_db_project_comment(conn, project_name + "_base") + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v3' # update some feature from 'base' db to create mismatch with src geopackage and modified - cur.execute(f"SELECT * from {db_schema_base}.simple") + cur.execute(sql.SQL("SELECT * from {}.simple").format(sql.Identifier(db_schema_base))) fid = cur.fetchone()[0] old_value = cur.fetchone()[3] - cur.execute(f"UPDATE {db_schema_base}.simple SET rating=100 WHERE fid={fid}") + cur.execute(sql.SQL("UPDATE {}.simple SET rating=100 WHERE fid=%s").format(sql.Identifier(db_schema_base)), (fid,)) conn.commit() - cur.execute(f"SELECT * from {db_schema_base}.simple WHERE fid={fid}") + cur.execute(sql.SQL("SELECT * from {}.simple WHERE fid=%s").format(sql.Identifier(db_schema_base)), (fid,)) assert cur.fetchone()[3] == 100 with pytest.raises(DbSyncError) as err: dbsync_init(mc, from_gpkg=True) @@ -217,8 +217,7 @@ def test_basic_pull(mc: MerginClient, project_name: str): 2. run init, check that everything is fine 3. make change in gpkg (copy new version), check everything is fine """ - project_name_main = _add_quotes_to_schema_name(project_name + "_main") - project_name_base = _add_quotes_to_schema_name(project_name + "_base") + db_schema_main = project_name + "_main" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -229,7 +228,7 @@ def test_basic_pull(mc: MerginClient, project_name: str): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 3 # make change in GPKG and push @@ -241,7 +240,7 @@ def test_basic_pull(mc: MerginClient, project_name: str): # check that a feature has been inserted cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format((sql.Identifier(db_schema_main)))) assert cur.fetchone()[0] == 4 db_proj_info = _get_db_project_comment(conn, project_name + "_base") assert db_proj_info["version"] == 'v2' @@ -253,7 +252,7 @@ def test_basic_pull(mc: MerginClient, project_name: str): @pytest.mark.parametrize("project_name", ['test_sync_push', 'Test_Sync_Push']) def test_basic_push(mc: MerginClient, project_name: str): """ Initialize a project and test push of a new row from PostgreSQL to Mergin Maps""" - project_name_main = _add_quotes_to_schema_name(project_name + "_main") + db_schema_main = project_name + "_main" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -264,14 +263,14 @@ def test_basic_push(mc: MerginClient, project_name: str): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 3 # make a change in PostgreSQL cur = conn.cursor() - cur.execute(f"INSERT INTO {project_name_main}.simple (name, rating) VALUES ('insert in postgres', 123)") + cur.execute(sql.SQL("INSERT INTO {}.simple (name, rating) VALUES ('insert in postgres', 123)").format(sql.Identifier(db_schema_main))) cur.execute("COMMIT") - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 4 # push the change from DB to PostgreSQL @@ -298,7 +297,8 @@ def test_basic_both(mc: MerginClient, project_name: str): and lets DB sync handle it: changes in PostgreSQL need to be rebased on top of changes in Mergin Maps server. """ - project_name_main = _add_quotes_to_schema_name(project_name + "_main") + db_schema_main = project_name + "_main" + db_schema_base = project_name + "_base" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory @@ -309,7 +309,7 @@ def test_basic_both(mc: MerginClient, project_name: str): # test that database schemas are created + tables are populated cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 3 # make change in GPKG and push @@ -318,17 +318,17 @@ def test_basic_both(mc: MerginClient, project_name: str): # make a change in PostgreSQL cur = conn.cursor() - cur.execute(f"INSERT INTO {project_name_main}.simple (name, rating) VALUES ('insert in postgres', 123)") + cur.execute(sql.SQL("INSERT INTO {}.simple (name, rating) VALUES ('insert in postgres', 123)").format(sql.Identifier(db_schema_main))) cur.execute("COMMIT") - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 4 # first pull changes from Mergin Maps to DB (+rebase changes in DB) and then push the changes from DB to Mergin Maps dbsync_pull(mc) - db_proj_info = _get_db_project_comment(conn, project_name + '_base') + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v2' dbsync_push(mc) - db_proj_info = _get_db_project_comment(conn, project_name + '_base') + db_proj_info = _get_db_project_comment(conn, db_schema_base) assert db_proj_info["version"] == 'v3' # pull new version of the project to the work project directory @@ -342,7 +342,7 @@ def test_basic_both(mc: MerginClient, project_name: str): # check that the insert has been applied to the DB cur = conn.cursor() - cur.execute(f"SELECT count(*) from {project_name_main}.simple") + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 5 print("---") @@ -354,24 +354,24 @@ def test_init_with_skip(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base_2tables.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') - db_schema_main = _add_quotes_to_schema_name(project_name + '_main') - db_schema_base = _add_quotes_to_schema_name(project_name + '_base') + db_schema_main = project_name + '_main' + db_schema_base = project_name + '_base' init_sync_from_geopackage(mc, project_name, source_gpkg_path, ["lines"]) # test that database schemas does not have ignored table conn = psycopg2.connect(DB_CONNINFO) cur = conn.cursor() - cur.execute(f"SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{db_schema_main}' AND tablename = 'lines');") + cur.execute(sql.SQL("SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{}' AND tablename = 'lines');").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == False - cur.execute(f"SELECT count(*) from {db_schema_main}.points") + cur.execute(sql.SQL("SELECT count(*) from {}.points").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 0 # run again, nothing should change dbsync_init(mc, from_gpkg=True) - cur.execute(f"SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{db_schema_main}' AND tablename = 'lines');") + cur.execute(sql.SQL("SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{}' AND tablename = 'lines');").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == False - cur.execute(f"SELECT count(*) from {db_schema_main}.points") + cur.execute(sql.SQL("SELECT count(*) from {}.points").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 0 # make change in GPKG and push to server to create pending changes, it should pass but not sync @@ -380,9 +380,9 @@ def test_init_with_skip(mc: MerginClient, project_name: str): # pull server changes to db to make sure only points table is updated dbsync_pull(mc) - cur.execute(f"SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{db_schema_main}' AND tablename = 'lines');") + cur.execute(sql.SQL("SELECT EXISTS (SELECT FROM pg_tables WHERE schemaname = '{}' AND tablename = 'lines');").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == False - cur.execute(f"SELECT count(*) from {db_schema_main}.points") + cur.execute(sql.SQL("SELECT count(*) from {}.points").format(sql.Identifier(db_schema_main))) assert cur.fetchone()[0] == 4 From dca2b59661da5e64631504a050b9360e6cb6e5d9 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 10:18:37 +0100 Subject: [PATCH 08/12] add to avoid accidental commits --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index afcc796..b01f141 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ config.ini mergin .coverage htmlcov +.vscode +.env \ No newline at end of file From c74ea2dc3300353c13b52a5f09ed1156a1cf3ce3 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 14:11:18 +0100 Subject: [PATCH 09/12] escape if schema contains other characters then a-z 0-9 and _, use double quote if necessary --- dbsync.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/dbsync.py b/dbsync.py index 58dd76c..2cfa736 100644 --- a/dbsync.py +++ b/dbsync.py @@ -16,6 +16,7 @@ import tempfile import random import uuid +import re import psycopg2 from itertools import chain @@ -35,7 +36,9 @@ class DbSyncError(Exception): def _add_quotes_to_schema_name(schema: str) -> str: - if any(ele.isupper() for ele in schema): + matches = re.findall(r"[^a-z0-9_]", schema) + if len(matches) != 0: + schema = schema.replace("\"", "\"\"") schema = f'"{schema}"' return schema From 904a88ef3144fb25df77e8b3715d538ec3d20fd0 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 14:12:21 +0100 Subject: [PATCH 10/12] rename tests to avoid potential issues on Windows --- test/test_basic.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/test_basic.py b/test/test_basic.py index 1f5b69b..bded2f9 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -97,7 +97,8 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables dbsync_init(mc, from_gpkg=True) -@pytest.mark.parametrize("project_name", ['test_init', 'Test_Init']) + +@pytest.mark.parametrize("project_name", ['test_init_1', 'Test_Init_2', "Test 3", "Test-4"]) def test_init_from_gpkg(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') @@ -196,7 +197,7 @@ def test_init_from_gpkg(mc: MerginClient, project_name: str): assert "There are pending changes in the local directory - that should never happen" in str(err.value) -@pytest.mark.parametrize("project_name", ['test_init', 'Test_init']) +@pytest.mark.parametrize("project_name", ['test_init_incomplete_dir_1', 'Test_Init_Incomplete_Dir_2']) def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') init_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync', project_name) @@ -209,7 +210,7 @@ def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient, project_name: str) assert os.listdir(init_project_dir) == ['test_sync.gpkg', '.mergin'] -@pytest.mark.parametrize("project_name", ['test_sync_pull', 'Test_Sync_Pull']) +@pytest.mark.parametrize("project_name", ['test_sync_pull_1', 'Test_Sync_Pull_2']) def test_basic_pull(mc: MerginClient, project_name: str): """ Test initialization and one pull from Mergin Maps to DB @@ -249,7 +250,7 @@ def test_basic_pull(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_sync_push', 'Test_Sync_Push']) +@pytest.mark.parametrize("project_name", ['test_sync_push_1', 'Test_Sync_Push_2']) def test_basic_push(mc: MerginClient, project_name: str): """ Initialize a project and test push of a new row from PostgreSQL to Mergin Maps""" db_schema_main = project_name + "_main" @@ -291,7 +292,7 @@ def test_basic_push(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_sync_both', 'Test_Sync_Both']) +@pytest.mark.parametrize("project_name", ['test_sync_both_1', 'Test_Sync_Both_2']) def test_basic_both(mc: MerginClient, project_name: str): """ Initializes a sync project and does both a change in Mergin Maps and in the database, and lets DB sync handle it: changes in PostgreSQL need to be rebased on top of @@ -349,7 +350,7 @@ def test_basic_both(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_init_skip', 'Test_Init_Skip']) +@pytest.mark.parametrize("project_name", ['test_init_skip_1', 'Test_Init_Skip_2']) def test_init_with_skip(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base_2tables.gpkg') @@ -386,7 +387,7 @@ def test_init_with_skip(mc: MerginClient, project_name: str): assert cur.fetchone()[0] == 4 -@pytest.mark.parametrize("project_name", ['test_local_changes', 'Test_Local_Changes']) +@pytest.mark.parametrize("project_name", ['test_local_changes_1', 'Test_Local_Changes_2']) def test_with_local_changes(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') @@ -418,7 +419,7 @@ def test_with_local_changes(mc: MerginClient, project_name: str): assert any(local_changes.values()) is False dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_recreated_project_ids', 'Test_Recreated_Project_Ids']) +@pytest.mark.parametrize("project_name", ['test_recreated_project_ids_1', 'Test_Recreated_Project_Ids_2']) def test_recreated_project_ids(mc: MerginClient, project_name: str): source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') From 7287c69b830eda39a7d8171fb7f38db42418e633 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 14:27:57 +0100 Subject: [PATCH 11/12] do not parameterize all the tests --- test/test_basic.py | 34 ++++++++++++++++------------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/test/test_basic.py b/test/test_basic.py index bded2f9..67d7718 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -98,8 +98,8 @@ def init_sync_from_geopackage(mc, project_name, source_gpkg_path, ignored_tables dbsync_init(mc, from_gpkg=True) -@pytest.mark.parametrize("project_name", ['test_init_1', 'Test_Init_2', "Test 3", "Test-4"]) -def test_init_from_gpkg(mc: MerginClient, project_name: str): +def test_init_from_gpkg(mc: MerginClient): + project_name = "test_init" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') db_schema_main = project_name + '_main' @@ -197,8 +197,8 @@ def test_init_from_gpkg(mc: MerginClient, project_name: str): assert "There are pending changes in the local directory - that should never happen" in str(err.value) -@pytest.mark.parametrize("project_name", ['test_init_incomplete_dir_1', 'Test_Init_Incomplete_Dir_2']) -def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient, project_name: str): +def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient): + project_name = "test_init_incomplete_dir" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') init_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync', project_name) init_sync_from_geopackage(mc, project_name, source_gpkg_path) @@ -210,14 +210,14 @@ def test_init_from_gpkg_with_incomplete_dir(mc: MerginClient, project_name: str) assert os.listdir(init_project_dir) == ['test_sync.gpkg', '.mergin'] -@pytest.mark.parametrize("project_name", ['test_sync_pull_1', 'Test_Sync_Pull_2']) -def test_basic_pull(mc: MerginClient, project_name: str): +def test_basic_pull(mc: MerginClient): """ Test initialization and one pull from Mergin Maps to DB 1. create a Mergin Maps project using py-client with a testing gpkg 2. run init, check that everything is fine 3. make change in gpkg (copy new version), check everything is fine """ + project_name = 'test_sync_pull' db_schema_main = project_name + "_main" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') @@ -250,9 +250,9 @@ def test_basic_pull(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_sync_push_1', 'Test_Sync_Push_2']) -def test_basic_push(mc: MerginClient, project_name: str): +def test_basic_push(mc: MerginClient): """ Initialize a project and test push of a new row from PostgreSQL to Mergin Maps""" + project_name = "test_sync_push" db_schema_main = project_name + "_main" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') @@ -292,12 +292,12 @@ def test_basic_push(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_sync_both_1', 'Test_Sync_Both_2']) -def test_basic_both(mc: MerginClient, project_name: str): +def test_basic_both(mc: MerginClient): """ Initializes a sync project and does both a change in Mergin Maps and in the database, and lets DB sync handle it: changes in PostgreSQL need to be rebased on top of changes in Mergin Maps server. """ + project_name = "test_sync_both" db_schema_main = project_name + "_main" db_schema_base = project_name + "_base" @@ -350,9 +350,8 @@ def test_basic_both(mc: MerginClient, project_name: str): dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_init_skip_1', 'Test_Init_Skip_2']) -def test_init_with_skip(mc: MerginClient, project_name: str): - +def test_init_with_skip(mc: MerginClient): + project_name = "test_init_skip" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base_2tables.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') db_schema_main = project_name + '_main' @@ -387,9 +386,8 @@ def test_init_with_skip(mc: MerginClient, project_name: str): assert cur.fetchone()[0] == 4 -@pytest.mark.parametrize("project_name", ['test_local_changes_1', 'Test_Local_Changes_2']) -def test_with_local_changes(mc: MerginClient, project_name: str): - +def test_with_local_changes(mc: MerginClient): + project_name = "test_local_changes" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') extra_files = [os.path.join(TEST_DATA_DIR, f) for f in ["note_1.txt", "note_3.txt", "modified_all.gpkg"]] dbsync_project_dir = os.path.join(TMP_DIR, project_name + '_dbsync', @@ -419,9 +417,9 @@ def test_with_local_changes(mc: MerginClient, project_name: str): assert any(local_changes.values()) is False dbsync_status(mc) -@pytest.mark.parametrize("project_name", ['test_recreated_project_ids_1', 'Test_Recreated_Project_Ids_2']) -def test_recreated_project_ids(mc: MerginClient, project_name: str): +def test_recreated_project_ids(mc: MerginClient): + project_name = "test_recreated_project_ids" source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') project_dir = os.path.join(TMP_DIR, project_name + '_work') # working directory full_project_name = WORKSPACE + "/" + project_name From b864614ad698d4be97b20c9b212ccd05cd09eb52 Mon Sep 17 00:00:00 2001 From: Jan Caha Date: Wed, 22 Mar 2023 14:30:23 +0100 Subject: [PATCH 12/12] create simple parametrized test, that contains just init, push and pull for couple of different project names --- test/test_basic.py | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/test/test_basic.py b/test/test_basic.py index 67d7718..9a35c71 100644 --- a/test/test_basic.py +++ b/test/test_basic.py @@ -438,3 +438,51 @@ def test_recreated_project_ids(mc: MerginClient): assert local_project_id != server_project_id with pytest.raises(DbSyncError): dbsync_status(mc) + +@pytest.mark.parametrize("project_name", ['test_init_1', 'Test_Init_2', "Test 3", "Test-4"]) +def test_project_names(mc: MerginClient, project_name: str): + source_gpkg_path = os.path.join(TEST_DATA_DIR, 'base.gpkg') + project_dir = os.path.join(TMP_DIR, project_name + '_work') + db_schema_main = project_name + '_main' + db_schema_base = project_name + '_base' + + init_sync_from_geopackage(mc, project_name, source_gpkg_path) + + # test that database schemas are created + tables are populated + conn = psycopg2.connect(DB_CONNINFO) + cur = conn.cursor() + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) + assert cur.fetchone()[0] == 3 + + # make change in GPKG and push + shutil.copy(os.path.join(TEST_DATA_DIR, 'inserted_1_A.gpkg'), os.path.join(project_dir, 'test_sync.gpkg')) + mc.push_project(project_dir) + + # pull server changes to db to make sure we can sync again + dbsync_pull(mc) + cur.execute(sql.SQL("SELECT count(*) from {}.simple").format(sql.Identifier(db_schema_main)).as_string(conn)) + assert cur.fetchone()[0] == 4 + db_proj_info = _get_db_project_comment(conn, db_schema_base) + assert db_proj_info["version"] == 'v2' + + # update some feature from 'modified' db to create mismatch with src geopackage, it should pass but not sync + fid = 1 + cur.execute(sql.SQL("SELECT * from {}.simple WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) + old_value = cur.fetchone()[3] + cur.execute(sql.SQL("UPDATE {}.simple SET rating=100 WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) + conn.commit() + cur.execute(sql.SQL("SELECT * from {}.simple WHERE fid=%s").format(sql.Identifier(db_schema_main)), (fid,)) + assert cur.fetchone()[3] == 100 + dbsync_init(mc, from_gpkg=True) + # check geopackage has not been modified - after init we are not synced! + gpkg_conn = sqlite3.connect(os.path.join(project_dir, 'test_sync.gpkg')) + gpkg_cur = gpkg_conn.cursor() + gpkg_cur.execute(f"SELECT * FROM simple WHERE fid={fid}") + assert gpkg_cur.fetchone()[3] == old_value + # push db changes to server (and download new version to local working dir) to make sure we can sync again + dbsync_push(mc) + mc.pull_project(project_dir) + gpkg_cur.execute(f"SELECT * FROM simple WHERE fid ={fid}") + assert gpkg_cur.fetchone()[3] == 100 + db_proj_info = _get_db_project_comment(conn, db_schema_base) + assert db_proj_info["version"] == 'v3'