From 69637a8dc989e20958ebd58fff511f0a1ee44918 Mon Sep 17 00:00:00 2001 From: Martin Dobias Date: Fri, 8 Oct 2021 17:26:15 +0200 Subject: [PATCH] Fix date/time handling when string values are not the same (fixes #143) This fixes the issue when applying a diff with date/time values, but the representation of date/time values as strings is slightly different even though they refer to the same date/time. Fixes both the case with update and the case with delete. Insert does not need fixing as there is no comparison with previous values. --- geodiff/src/drivers/sqlitedriver.cpp | 12 ++++++++ geodiff/tests/test_driver_sqlite.cpp | 27 ++++++++++++++++++ .../tests/testdata/datetime/datetime1-3a.diff | Bin 0 -> 97 bytes .../tests/testdata/datetime/datetime1a.gpkg | Bin 0 -> 98304 bytes .../tests/testdata/datetime/datetime3.gpkg | Bin 0 -> 98304 bytes 5 files changed, 39 insertions(+) create mode 100644 geodiff/tests/testdata/datetime/datetime1-3a.diff create mode 100644 geodiff/tests/testdata/datetime/datetime1a.gpkg create mode 100644 geodiff/tests/testdata/datetime/datetime3.gpkg diff --git a/geodiff/src/drivers/sqlitedriver.cpp b/geodiff/src/drivers/sqlitedriver.cpp index 23f37481..3fa993bd 100644 --- a/geodiff/src/drivers/sqlitedriver.cpp +++ b/geodiff/src/drivers/sqlitedriver.cpp @@ -629,6 +629,12 @@ static std::string sqlForUpdate( const std::string &tableName, const TableSchema sql += " AND "; if ( tbl.columns[i].isPrimaryKey ) sql += sqlitePrintf( " \"%w\" = ?%d ", tbl.columns[i].name.c_str(), i * 3 + 1 ); + else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME ) + { + // compare date/time values using datetime() because they may have + // multiple equivalent string representations (see #143) + sql += sqlitePrintf( " ( ?%d = 0 OR datetime(\"%w\") IS datetime(?%d) ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 ); + } else sql += sqlitePrintf( " ( ?%d = 0 OR \"%w\" IS ?%d ) ", i * 3 + 2, tbl.columns[i].name.c_str(), i * 3 + 1 ); } @@ -652,6 +658,12 @@ static std::string sqlForDelete( const std::string &tableName, const TableSchema sql += " AND "; if ( tbl.columns[i].isPrimaryKey ) sql += sqlitePrintf( "\"%w\" = ?", tbl.columns[i].name.c_str() ); + else if ( tbl.columns[i].type.baseType == TableColumnType::DATETIME ) + { + // compare date/time values using datetime() because they may have + // multiple equivalent string representations (see #143) + sql += sqlitePrintf( "datetime(\"%w\") IS datetime(?)", tbl.columns[i].name.c_str() ); + } else sql += sqlitePrintf( "\"%w\" IS ?", tbl.columns[i].name.c_str() ); } diff --git a/geodiff/tests/test_driver_sqlite.cpp b/geodiff/tests/test_driver_sqlite.cpp index 1c47b05c..cd71dc20 100644 --- a/geodiff/tests/test_driver_sqlite.cpp +++ b/geodiff/tests/test_driver_sqlite.cpp @@ -293,6 +293,33 @@ TEST( SqliteDriverTest, create_changeset_datetime ) ); } + +TEST( SqliteDriverTest, apply_changeset_datetime ) +{ + // check that the datetime handling is robust - a single datetime may have + // multiple representations, e.g. '2021-03-16T00:00:00' and '2021-03-16T00:00:00Z' + // represent the same datetime, so things should work fine even if database + // contains '2021-03-16T00:00:00' but changeset refers to '2021-03-16T00:00:00Z' + + // datetime1-3a.diff refers to date/time value in deleted row as '2021-04-01T15:00:00Z' + // while datetime1.gpkg has the value stored as '2021-04-01 15:00:00' + testApplyChangeset( "test_apply_changeset_datetime_delete", + pathjoin( testdir(), "datetime", "datetime1.gpkg" ), + pathjoin( testdir(), "datetime", "datetime1-3a.diff" ), + pathjoin( testdir(), "datetime", "datetime3.gpkg" ) + ); + + // datetime1a.gpkg has the same content as datetime1, but date/time values + // are represented as '2021-04-01T15:00:00Z' instead of '2021-04-01 15:00:00' + // (which is used in datetime1.gpkg and in datetime1-2.diff) + testApplyChangeset( "test_apply_changeset_datetime_update", + pathjoin( testdir(), "datetime", "datetime1a.gpkg" ), + pathjoin( testdir(), "datetime", "datetime1-2.diff" ), + pathjoin( testdir(), "datetime", "datetime2.gpkg" ) + ); +} + + TEST( SqliteDriverTest, apply_with_gpkg_contents ) { // In geodiff >= 1.0 we ignore gpkg_* metadata tables. However older geodiff diff --git a/geodiff/tests/testdata/datetime/datetime1-3a.diff b/geodiff/tests/testdata/datetime/datetime1-3a.diff new file mode 100644 index 0000000000000000000000000000000000000000..50f229eeccc71d12681e8b215445fcf4792363ae GIT binary patch literal 97 zcmWGxWn=(?;>_HFoKyx*1~3mOz{n!&9>Bo(OaRCPiOE%*UTBu{aeu+HRVOSP@7gnS jq@^a7loq8L!gVl<7#SED>Kd5n8W@Hcnpznc08tbG%g+@6 literal 0 HcmV?d00001 diff --git a/geodiff/tests/testdata/datetime/datetime1a.gpkg b/geodiff/tests/testdata/datetime/datetime1a.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..c096fd02df797a25daf34649ae483a260164528f GIT binary patch literal 98304 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 literal 0 HcmV?d00001 diff --git a/geodiff/tests/testdata/datetime/datetime3.gpkg b/geodiff/tests/testdata/datetime/datetime3.gpkg new file mode 100644 index 0000000000000000000000000000000000000000..d3cfbc7827725dce5bdd0dfbac3ff3bdcfde67a2 GIT binary patch literal 98304 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 Y^dXAgA)!-r$1l5F{*jCR(t(TqzYZgbk^lez literal 0 HcmV?d00001