Skip to content

Commit 4910b73

Browse files
committed
[WFS provider] Ensure stability of QGIS FeatureId when reloading layer (fixes #20865)
1 parent 76c48e6 commit 4910b73

File tree

4 files changed

+243
-31
lines changed

4 files changed

+243
-31
lines changed

src/providers/wfs/qgswfsfeatureiterator.cpp

+25-1
Original file line numberDiff line numberDiff line change
@@ -32,13 +32,16 @@
3232
#include "qgssettings.h"
3333
#include "qgsexception.h"
3434
#include "qgsfeedback.h"
35+
#include "qgssqliteutils.h"
3536

3637
#include <algorithm>
3738
#include <QDir>
3839
#include <QProgressDialog>
3940
#include <QTimer>
4041
#include <QStyle>
4142

43+
#include <sqlite3.h>
44+
4245
QgsWFSFeatureHitsAsyncRequest::QgsWFSFeatureHitsAsyncRequest( QgsWFSDataSourceURI &uri )
4346
: QgsWfsRequest( uri )
4447
, mNumberMatched( -1 )
@@ -986,7 +989,15 @@ QgsFeatureRequest QgsWFSFeatureIterator::buildRequestCache( int genCounter )
986989
QgsFeatureRequest requestCache;
987990
if ( mRequest.filterType() == QgsFeatureRequest::FilterFid ||
988991
mRequest.filterType() == QgsFeatureRequest::FilterFids )
989-
requestCache = mRequest;
992+
{
993+
QgsFeatureIds qgisIds;
994+
if ( mRequest.filterType() == QgsFeatureRequest::FilterFid )
995+
qgisIds.insert( mRequest.filterFid() );
996+
else
997+
qgisIds = mRequest.filterFids();
998+
999+
requestCache.setFilterFids( mShared->dbIdsFromQgisIds( qgisIds ) );
1000+
}
9901001
else
9911002
{
9921003
if ( mRequest.filterType() == QgsFeatureRequest::FilterExpression )
@@ -1238,6 +1249,19 @@ bool QgsWFSFeatureIterator::fetchFeature( QgsFeature &f )
12381249

12391250
copyFeature( cachedFeature, f );
12401251
geometryToDestinationCrs( f, mTransform );
1252+
1253+
// Retrieve the user-visible id from the Spatialite cache database Id
1254+
if ( mShared->mCacheIdDb.get() )
1255+
{
1256+
auto sql = QgsSqlite3Mprintf( "SELECT qgisId FROM id_cache WHERE dbId = %lld", cachedFeature.id() );
1257+
int resultCode;
1258+
auto stmt = mShared->mCacheIdDb.prepare( sql, resultCode );
1259+
if ( stmt.step() == SQLITE_ROW )
1260+
{
1261+
f.setId( stmt.columnAsInt64( 0 ) );
1262+
}
1263+
}
1264+
12411265
return true;
12421266
}
12431267

src/providers/wfs/qgswfsshareddata.cpp

+176-30
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,16 @@ QgsWFSSharedData::~QgsWFSSharedData()
6262
QgsDebugMsgLevel( QStringLiteral( "~QgsWFSSharedData()" ), 4 );
6363

6464
invalidateCache();
65+
66+
mCacheIdDb.reset();
67+
if ( !mCacheIdDbname.isEmpty() )
68+
{
69+
QFile::remove( mCacheIdDbname );
70+
QFile::remove( mCacheIdDbname + "-wal" );
71+
QFile::remove( mCacheIdDbname + "-shm" );
72+
QgsWFSUtils::releaseCacheDirectory();
73+
mCacheIdDbname.clear();
74+
}
6575
}
6676

6777
QString QgsWFSSharedData::srsName() const
@@ -215,7 +225,8 @@ bool QgsWFSSharedData::createCache()
215225

216226
static QAtomicInt sTmpCounter = 0;
217227
int tmpCounter = ++sTmpCounter;
218-
mCacheDbname = QDir( QgsWFSUtils::acquireCacheDirectory() ).filePath( QStringLiteral( "wfs_cache_%1.sqlite" ).arg( tmpCounter ) );
228+
QString cacheDirectory( QgsWFSUtils::acquireCacheDirectory() );
229+
mCacheDbname = QDir( cacheDirectory ).filePath( QStringLiteral( "wfs_cache_%1.sqlite" ).arg( tmpCounter ) );
219230
Q_ASSERT( !QFile::exists( mCacheDbname ) );
220231

221232
QgsFields cacheFields;
@@ -466,6 +477,35 @@ bool QgsWFSSharedData::createCache()
466477
return false;
467478
}
468479

480+
// The id_cache should be generated once for the lifetime of QgsWFSConstants
481+
// to ensure consistency of the ids returned to the user.
482+
if ( mCacheIdDbname.isEmpty() )
483+
{
484+
mCacheIdDbname = QDir( cacheDirectory ).filePath( QStringLiteral( "wfs_id_cache_%1.sqlite" ).arg( tmpCounter ) );
485+
Q_ASSERT( !QFile::exists( mCacheIdDbname ) );
486+
if ( mCacheIdDb.open( mCacheIdDbname ) != SQLITE_OK )
487+
{
488+
QgsMessageLog::logMessage( tr( "Cannot create temporary id cache" ), tr( "WFS" ) );
489+
return false;
490+
}
491+
QString errorMsg;
492+
bool ok = mCacheIdDb.exec( QStringLiteral( "PRAGMA synchronous=OFF" ), errorMsg ) == SQLITE_OK;
493+
// WAL is needed to avoid reader to block writers
494+
ok &= mCacheIdDb.exec( QStringLiteral( "PRAGMA journal_mode=WAL" ), errorMsg ) == SQLITE_OK;
495+
// gmlid is the gmlid or fid attribute coming from the GML GetFeature response
496+
// qgisId is the feature id of the features returned to QGIS. That one should remain the same for a given gmlid even after a layer reload
497+
// dbId is the feature id of the Spatialite feature in mCacheDataProvider. It might change for a given gmlid after a layer reload
498+
ok &= mCacheIdDb.exec( QStringLiteral( "CREATE TABLE id_cache(gmlid TEXT, dbId INTEGER, qgisId INTEGER)" ), errorMsg ) == SQLITE_OK;
499+
ok &= mCacheIdDb.exec( QStringLiteral( "CREATE INDEX idx_gmlid ON id_cache(gmlid)" ), errorMsg ) == SQLITE_OK;
500+
ok &= mCacheIdDb.exec( QStringLiteral( "CREATE INDEX idx_dbId ON id_cache(dbId)" ), errorMsg ) == SQLITE_OK;
501+
ok &= mCacheIdDb.exec( QStringLiteral( "CREATE INDEX idx_qgisId ON id_cache(qgisId)" ), errorMsg ) == SQLITE_OK;
502+
if ( !ok )
503+
{
504+
QgsDebugMsg( errorMsg );
505+
return false;
506+
}
507+
}
508+
469509
return true;
470510
}
471511

@@ -572,6 +612,10 @@ int QgsWFSSharedData::getUpdatedCounter()
572612

573613
QSet<QString> QgsWFSSharedData::getExistingCachedGmlIds( const QVector<QgsWFSFeatureGmlIdPair> &featureList )
574614
{
615+
// We query the Spatialite cache here, not the persistent id_cache,
616+
// since we want to know which features in this session we have already
617+
// downloaded.
618+
575619
QString expr;
576620
bool first = true;
577621
QSet<QString> setExistingGmlIds;
@@ -675,46 +719,79 @@ QSet<QString> QgsWFSSharedData::getExistingCachedMD5( const QVector<QgsWFSFeatur
675719
// Used by WFS-T
676720
QString QgsWFSSharedData::findGmlId( QgsFeatureId fid )
677721
{
678-
if ( !mCacheDataProvider )
722+
if ( !mCacheIdDb )
679723
return QString();
680-
QgsFeatureRequest request;
681-
request.setFilterFid( fid );
682724

683-
QgsFields dataProviderFields = mCacheDataProvider->fields();
684-
int gmlidIdx = dataProviderFields.indexFromName( QgsWFSConstants::FIELD_GMLID );
685-
686-
QgsAttributeList attList;
687-
attList.append( gmlidIdx );
688-
request.setSubsetOfAttributes( attList );
689-
690-
QgsFeatureIterator iterGmlIds( mCacheDataProvider->getFeatures( request ) );
691-
QgsFeature gmlidFeature;
692-
while ( iterGmlIds.nextFeature( gmlidFeature ) )
725+
auto sql = QgsSqlite3Mprintf( "SELECT gmlid FROM id_cache WHERE qgisId = %lld", fid );
726+
int resultCode;
727+
auto stmt = mCacheIdDb.prepare( sql, resultCode );
728+
Q_ASSERT( resultCode == SQLITE_OK );
729+
if ( stmt.step() == SQLITE_ROW )
693730
{
694-
const QVariant &v = gmlidFeature.attributes().value( gmlidIdx );
695-
return v.toString();
731+
return stmt.columnAsText( 0 );
696732
}
697733
return QString();
698734
}
699735

736+
QgsFeatureIds QgsWFSSharedData::dbIdsFromQgisIds( const QgsFeatureIds &qgisIds )
737+
{
738+
QgsFeatureIds dbIds;
739+
if ( !mCacheIdDb )
740+
return dbIds;
741+
// To avoid excessive memory consumption in expression building, do not
742+
// query more than 1000 ids at a time.
743+
bool first = true;
744+
QString expr;
745+
int i = 0;
746+
for ( const auto &qgisId : qgisIds )
747+
{
748+
if ( !first )
749+
expr += ',';
750+
else
751+
{
752+
expr = QStringLiteral( "SELECT dbId FROM id_cache WHERE qgisId IN (" );
753+
first = false;
754+
}
755+
expr += FID_TO_STRING( qgisId );
756+
757+
if ( ( i > 0 && ( i % 1000 ) == 0 ) || i + 1 == qgisIds.size() )
758+
{
759+
expr += ')';
760+
761+
int resultCode;
762+
auto stmt = mCacheIdDb.prepare( expr.toUtf8().constData(), resultCode );
763+
Q_ASSERT( resultCode == SQLITE_OK );
764+
while ( stmt.step() == SQLITE_ROW )
765+
{
766+
dbIds.insert( stmt.columnAsInt64( 0 ) );
767+
}
768+
// Should we check that we got a dbId from every qgisId... ?
769+
770+
first = true;
771+
}
772+
i++;
773+
}
774+
return dbIds;
775+
}
776+
700777
// Used by WFS-T
701778
bool QgsWFSSharedData::deleteFeatures( const QgsFeatureIds &fidlist )
702779
{
703-
if ( !mCacheDataProvider )
780+
if ( !mCacheIdDb || !mCacheDataProvider )
704781
return false;
705782

706783
{
707784
QMutexLocker locker( &mMutex );
708785
mFeatureCount -= fidlist.size();
709786
}
710787

711-
return mCacheDataProvider->deleteFeatures( fidlist );
788+
return mCacheDataProvider->deleteFeatures( dbIdsFromQgisIds( fidlist ) );
712789
}
713790

714791
// Used by WFS-T
715792
bool QgsWFSSharedData::changeGeometryValues( const QgsGeometryMap &geometry_map )
716793
{
717-
if ( !mCacheDataProvider )
794+
if ( !mCacheIdDb || !mCacheDataProvider )
718795
return false;
719796

720797
// We need to replace the geometry by its bounding box and issue a attribute
@@ -727,22 +804,34 @@ bool QgsWFSSharedData::changeGeometryValues( const QgsGeometryMap &geometry_map
727804
QgsChangedAttributesMap newChangedAttrMap;
728805
for ( QgsGeometryMap::const_iterator iter = geometry_map.constBegin(); iter != geometry_map.constEnd(); ++iter )
729806
{
807+
auto sql = QgsSqlite3Mprintf( "SELECT dbId FROM id_cache WHERE qgisId = %lld", iter.key() );
808+
int resultCode;
809+
auto stmt = mCacheIdDb.prepare( sql, resultCode );
810+
Q_ASSERT( resultCode == SQLITE_OK );
811+
if ( stmt.step() != SQLITE_ROW )
812+
{
813+
// shouldn't happen normally
814+
QgsDebugMsg( QStringLiteral( "cannot find dbId corresponding to qgisId = %1" ).arg( iter.key() ) );
815+
continue;
816+
}
817+
QgsFeatureId dbId = stmt.columnAsInt64( 0 );
818+
730819
QByteArray wkb = iter->asWkb();
731820
if ( !wkb.isEmpty() )
732821
{
733822
QgsAttributeMap newAttrMap;
734823
newAttrMap[idx] = QString( wkb.toHex().data() );
735-
newChangedAttrMap[ iter.key()] = newAttrMap;
824+
newChangedAttrMap[ dbId] = newAttrMap;
736825

737826
QgsGeometry polyBoundingBox = QgsGeometry::fromRect( iter.value().boundingBox() );
738-
newGeometryMap[ iter.key()] = polyBoundingBox;
827+
newGeometryMap[ dbId] = polyBoundingBox;
739828
}
740829
else
741830
{
742831
QgsAttributeMap newAttrMap;
743832
newAttrMap[idx] = QString();
744-
newChangedAttrMap[ iter.key()] = newAttrMap;
745-
newGeometryMap[ iter.key()] = QgsGeometry();
833+
newChangedAttrMap[ dbId] = newAttrMap;
834+
newGeometryMap[ dbId] = QgsGeometry();
746835
}
747836
}
748837

@@ -753,14 +842,25 @@ bool QgsWFSSharedData::changeGeometryValues( const QgsGeometryMap &geometry_map
753842
// Used by WFS-T
754843
bool QgsWFSSharedData::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
755844
{
756-
if ( !mCacheDataProvider )
845+
if ( !mCacheIdDb || !mCacheDataProvider )
757846
return false;
758847

759848
QgsFields dataProviderFields = mCacheDataProvider->fields();
760849
QgsChangedAttributesMap newMap;
761850
for ( QgsChangedAttributesMap::const_iterator iter = attr_map.begin(); iter != attr_map.end(); ++iter )
762851
{
763-
QgsFeatureId fid = iter.key();
852+
auto sql = QgsSqlite3Mprintf( "SELECT dbId FROM id_cache WHERE qgisId = %lld", iter.key() );
853+
int resultCode;
854+
auto stmt = mCacheIdDb.prepare( sql, resultCode );
855+
Q_ASSERT( resultCode == SQLITE_OK );
856+
if ( stmt.step() != SQLITE_ROW )
857+
{
858+
// shouldn't happen normally
859+
QgsDebugMsg( QStringLiteral( "cannot find dbId corresponding to qgisId = %1" ).arg( iter.key() ) );
860+
continue;
861+
}
862+
QgsFeatureId dbId = stmt.columnAsInt64( 0 );
863+
764864
const QgsAttributeMap &attrs = iter.value();
765865
if ( attrs.isEmpty() )
766866
continue;
@@ -774,7 +874,7 @@ bool QgsWFSSharedData::changeAttributeValues( const QgsChangedAttributesMap &att
774874
else
775875
newAttrMap[idx] = siter.value();
776876
}
777-
newMap[fid] = newAttrMap;
877+
newMap[dbId] = newAttrMap;
778878
}
779879

780880
return mCacheDataProvider->changeAttributeValues( newMap );
@@ -923,10 +1023,57 @@ void QgsWFSSharedData::serializeFeatures( QVector<QgsWFSFeatureGmlIdPair> &featu
9231023
Q_ASSERT( featureListToCache.size() == updatedFeatureList.size() );
9241024
for ( int i = 0; i < updatedFeatureList.size(); i++ )
9251025
{
926-
if ( cacheOk )
927-
updatedFeatureList[i].first.setId( featureListToCache[i].id() );
1026+
int resultCode;
1027+
QgsFeatureId dbId( cacheOk ? featureListToCache[i].id() : mTotalFeaturesAttemptedToBeCached + i + 1 );
1028+
QgsFeatureId qgisId;
1029+
const auto &gmlId( updatedFeatureList[i].second );
1030+
if ( gmlId.isEmpty() )
1031+
{
1032+
// Degraded case. Won't work properly in reload situations, but we
1033+
// can't do better.
1034+
qgisId = dbId;
1035+
}
9281036
else
929-
updatedFeatureList[i].first.setId( mTotalFeaturesAttemptedToBeCached + i + 1 );
1037+
{
1038+
auto sql = QgsSqlite3Mprintf( "SELECT qgisId, dbId FROM id_cache WHERE gmlid = '%q'",
1039+
gmlId.toUtf8().constData() );
1040+
auto stmt = mCacheIdDb.prepare( sql, resultCode );
1041+
Q_ASSERT( resultCode == SQLITE_OK );
1042+
if ( stmt.step() == SQLITE_ROW )
1043+
{
1044+
qgisId = stmt.columnAsInt64( 0 );
1045+
QgsFeatureId oldDbId = stmt.columnAsInt64( 1 );
1046+
if ( dbId != oldDbId )
1047+
{
1048+
sql = QgsSqlite3Mprintf( "UPDATE id_cache SET dbId = %lld WHERE gmlid = '%q'",
1049+
dbId,
1050+
gmlId.toUtf8().constData() );
1051+
//QgsDebugMsg( QStringLiteral( "%1" ).arg( sql ) );
1052+
QString errorMsg;
1053+
if ( mCacheIdDb.exec( sql, errorMsg ) != SQLITE_OK )
1054+
{
1055+
QgsMessageLog::logMessage( tr( "Problem when updating WFS id cache: %1 -> %2" ).arg( sql ).arg( errorMsg ), tr( "WFS" ) );
1056+
}
1057+
}
1058+
}
1059+
else
1060+
{
1061+
qgisId = mNextCachedIdQgisId;
1062+
mNextCachedIdQgisId ++;
1063+
sql = QgsSqlite3Mprintf( "INSERT INTO id_cache (gmlid, dbId, qgisId) VALUES ('%q', %lld, %lld)",
1064+
gmlId.toUtf8().constData(),
1065+
dbId,
1066+
qgisId );
1067+
//QgsDebugMsg( QStringLiteral( "%1" ).arg( sql ) );
1068+
QString errorMsg;
1069+
if ( mCacheIdDb.exec( sql, errorMsg ) != SQLITE_OK )
1070+
{
1071+
QgsMessageLog::logMessage( tr( "Problem when updating WFS id cache: %1 -> %2" ).arg( sql ).arg( errorMsg ), tr( "WFS" ) );
1072+
}
1073+
}
1074+
}
1075+
1076+
updatedFeatureList[i].first.setId( qgisId );
9301077
}
9311078

9321079
{
@@ -1104,7 +1251,6 @@ void QgsWFSSharedData::invalidateCache()
11041251
QFile::remove( mCacheDbname );
11051252
QFile::remove( mCacheDbname + "-wal" );
11061253
QFile::remove( mCacheDbname + "-shm" );
1107-
QgsWFSUtils::releaseCacheDirectory();
11081254
mCacheDbname.clear();
11091255
}
11101256
}

0 commit comments

Comments
 (0)