Skip to content

Commit d7b85c6

Browse files
authoredJun 26, 2018
Merge pull request #7016 from m-kuhn/featureSourceEmpty
QgsFeatureSource::empty() method
2 parents 7847421 + ae6bffd commit d7b85c6

17 files changed

+254
-2
lines changed
 

‎python/core/auto_generated/processing/qgsprocessingutils.sip.in

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,9 @@ An optional ``request`` can be used to optimise the returned
260260
iterator, eg by restricting the returned attributes or geometry.
261261
%End
262262

263+
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const;
264+
265+
263266
virtual QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const;
264267

265268
virtual QgsCoordinateReferenceSystem sourceCrs() const;

‎python/core/auto_generated/qgsfeaturesource.sip.in

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,13 @@ An interface for objects which provide features via a getFeatures method.
2323
%End
2424
public:
2525

26+
enum FeatureAvailability
27+
{
28+
NoFeaturesAvailable,
29+
FeaturesAvailable,
30+
FeaturesMaybeAvailable
31+
};
32+
2633
virtual ~QgsFeatureSource();
2734

2835
virtual QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const = 0;
@@ -66,6 +73,13 @@ if the feature count is unknown.
6673
%Docstring
6774
Returns the number of features contained in the source, or -1
6875
if the feature count is unknown.
76+
%End
77+
78+
virtual FeatureAvailability hasFeatures() const;
79+
%Docstring
80+
Determines if there are any features available in the source.
81+
82+
.. versionadded:: 3.2
6983
%End
7084

7185
virtual QSet<QVariant> uniqueValues( int fieldIndex, int limit = -1 ) const;
@@ -139,7 +153,6 @@ for its ownership.
139153

140154
.. versionadded:: 3.0
141155
%End
142-
143156
};
144157

145158

‎python/core/auto_generated/qgsvectordataprovider.sip.in

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,26 @@ Returns the geometry type which is returned by this layer
122122
Number of features in the layer
123123

124124
:return: long containing number of features
125+
%End
126+
127+
virtual bool empty() const;
128+
%Docstring
129+
Returns true if the layer contains at least one feature.
130+
131+
.. versionadded:: 3.4
132+
%End
133+
134+
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const final;
135+
%Docstring
136+
Will always return FeatureAvailability.FeaturesAvailable or
137+
FeatureAvailability.NoFeaturesAvailable.
138+
139+
Calls empty() internally. Providers should override empty()
140+
instead if they provide an optimized version of this call.
141+
142+
.. seealso:: :py:func:`empty`
143+
144+
.. versionadded:: 3.4
125145
%End
126146

127147
virtual QgsFields fields() const = 0;

‎python/core/auto_generated/qgsvectorlayer.sip.in

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -941,6 +941,21 @@ Number of features rendered with specified legend key. Features must be first
941941
calculated by countSymbolFeatures()
942942

943943
:return: number of features rendered by symbol or -1 if failed or counts are not available
944+
%End
945+
946+
virtual FeatureAvailability hasFeatures() const;
947+
948+
%Docstring
949+
Determines if this vector layer has features.
950+
951+
.. warning::
952+
953+
when a layer is editable and some features
954+
have been deleted, this will return
955+
QgsFeatureSource.FeatureAvailability.FeaturesMayBeAvailable
956+
to avoid a potentially expensive call to the dataprovider.
957+
958+
.. versionadded:: 3.4
944959
%End
945960

946961
void setDataSource( const QString &dataSource, const QString &baseName, const QString &provider, bool loadDefaultStyleFlag = false ) /Deprecated/;

‎src/core/processing/qgsprocessingutils.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -721,6 +721,18 @@ QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequ
721721
return mSource->getFeatures( req );
722722
}
723723

724+
QgsFeatureSource::FeatureAvailability QgsProcessingFeatureSource::hasFeatures() const
725+
{
726+
FeatureAvailability sourceAvailability = mSource->hasFeatures();
727+
if ( sourceAvailability == NoFeaturesAvailable )
728+
return NoFeaturesAvailable; // never going to be features if underlying source has no features
729+
else if ( mInvalidGeometryCheck == QgsFeatureRequest::GeometryNoCheck )
730+
return sourceAvailability;
731+
else
732+
// we don't know... source has features, but these may be filtered out by invalid geometry check
733+
return FeaturesMaybeAvailable;
734+
}
735+
724736
QgsFeatureIterator QgsProcessingFeatureSource::getFeatures( const QgsFeatureRequest &request ) const
725737
{
726738
QgsFeatureRequest req( request );

‎src/core/processing/qgsprocessingutils.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,8 @@ class CORE_EXPORT QgsProcessingFeatureSource : public QgsFeatureSource
317317
*/
318318
QgsFeatureIterator getFeatures( const QgsFeatureRequest &request, Flags flags ) const;
319319

320+
QgsFeatureSource::FeatureAvailability hasFeatures() const override;
321+
320322
QgsFeatureIterator getFeatures( const QgsFeatureRequest &request = QgsFeatureRequest() ) const override;
321323
QgsCoordinateReferenceSystem sourceCrs() const override;
322324
QgsFields fields() const override;

‎src/core/qgsfeaturesource.cpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
#include "qgsvectorlayer.h"
2424
#include "qgsvectordataprovider.h"
2525

26+
QgsFeatureSource::FeatureAvailability QgsFeatureSource::hasFeatures() const
27+
{
28+
return FeaturesMaybeAvailable;
29+
}
30+
2631
QSet<QVariant> QgsFeatureSource::uniqueValues( int fieldIndex, int limit ) const
2732
{
2833
if ( fieldIndex < 0 || fieldIndex >= fields().count() )

‎src/core/qgsfeaturesource.h

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,22 @@ class CORE_EXPORT QgsFeatureSource
3838
{
3939
public:
4040

41+
/**
42+
* Possible return value for hasFeatures() to determine if a source is empty.
43+
* It is implemented as a three-value logic, so it can return if
44+
* there are features available for sure, if there are no features
45+
* available for sure or if there might be features available but
46+
* there is no guarantee for this.
47+
*
48+
* \since QGIS 3.4
49+
*/
50+
enum FeatureAvailability
51+
{
52+
NoFeaturesAvailable, //!< There are certainly no features available in this source
53+
FeaturesAvailable, //!< There is at least one feature available in this source
54+
FeaturesMaybeAvailable //!< There may be features available in this source
55+
};
56+
4157
virtual ~QgsFeatureSource() = default;
4258

4359
/**
@@ -85,6 +101,13 @@ class CORE_EXPORT QgsFeatureSource
85101
*/
86102
virtual long featureCount() const = 0;
87103

104+
/**
105+
* Determines if there are any features available in the source.
106+
*
107+
* \since QGIS 3.2
108+
*/
109+
virtual FeatureAvailability hasFeatures() const;
110+
88111
/**
89112
* Returns the set of unique values contained within the specified \a fieldIndex from this source.
90113
* If specified, the \a limit option can be used to limit the number of returned values.
@@ -150,7 +173,6 @@ class CORE_EXPORT QgsFeatureSource
150173
*/
151174
QgsVectorLayer *materialize( const QgsFeatureRequest &request,
152175
QgsFeedback *feedback = nullptr ) SIP_FACTORY;
153-
154176
};
155177

156178
Q_DECLARE_METATYPE( QgsFeatureSource * )

‎src/core/qgsvectordataprovider.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,27 @@ QString QgsVectorDataProvider::storageType() const
4646
return QStringLiteral( "Generic vector file" );
4747
}
4848

49+
bool QgsVectorDataProvider::empty() const
50+
{
51+
QgsFeature f;
52+
QgsFeatureRequest request;
53+
request.setSubsetOfAttributes( QgsAttributeList() );
54+
request.setFlags( QgsFeatureRequest::NoGeometry );
55+
request.setLimit( 1 );
56+
if ( getFeatures( request ).nextFeature( f ) )
57+
return false;
58+
else
59+
return true;
60+
}
61+
62+
QgsFeatureSource::FeatureAvailability QgsVectorDataProvider::hasFeatures() const
63+
{
64+
if ( empty() )
65+
return QgsFeatureSource::FeatureAvailability::NoFeaturesAvailable;
66+
else
67+
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
68+
}
69+
4970
QgsCoordinateReferenceSystem QgsVectorDataProvider::sourceCrs() const
5071
{
5172
return crs();

‎src/core/qgsvectordataprovider.h

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,25 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider, public QgsFeat
163163
*/
164164
long featureCount() const override = 0;
165165

166+
/**
167+
* Returns true if the layer contains at least one feature.
168+
*
169+
* \since QGIS 3.4
170+
*/
171+
virtual bool empty() const;
172+
173+
/**
174+
* Will always return FeatureAvailability::FeaturesAvailable or
175+
* FeatureAvailability::NoFeaturesAvailable.
176+
*
177+
* Calls empty() internally. Providers should override empty()
178+
* instead if they provide an optimized version of this call.
179+
*
180+
* \see empty()
181+
* \since QGIS 3.4
182+
*/
183+
virtual QgsFeatureSource::FeatureAvailability hasFeatures() const final;
184+
166185
/**
167186
* Returns the fields associated with this data provider.
168187
*/

‎src/core/qgsvectorlayer.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,8 @@ long QgsVectorLayer::featureCount( const QString &legendKey ) const
704704
return mSymbolFeatureCountMap.value( legendKey );
705705
}
706706

707+
708+
707709
QgsVectorLayerFeatureCounter *QgsVectorLayer::countSymbolFeatures()
708710
{
709711
if ( mSymbolFeatureCounted || mFeatureCounter )
@@ -2766,6 +2768,25 @@ long QgsVectorLayer::featureCount() const
27662768
( mEditBuffer ? mEditBuffer->mAddedFeatures.size() - mEditBuffer->mDeletedFeatureIds.size() : 0 );
27672769
}
27682770

2771+
QgsFeatureSource::FeatureAvailability QgsVectorLayer::hasFeatures() const
2772+
{
2773+
const QgsFeatureIds deletedFeatures( mEditBuffer ? mEditBuffer->deletedFeatureIds() : QgsFeatureIds() );
2774+
const QgsFeatureMap addedFeatures( mEditBuffer ? mEditBuffer->addedFeatures() : QgsFeatureMap() );
2775+
2776+
if ( mEditBuffer && !deletedFeatures.empty() )
2777+
{
2778+
if ( addedFeatures.size() > deletedFeatures.size() )
2779+
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
2780+
else
2781+
return QgsFeatureSource::FeatureAvailability::FeaturesMaybeAvailable;
2782+
}
2783+
2784+
if ( ( !mEditBuffer || addedFeatures.empty() ) && mDataProvider->empty() )
2785+
return QgsFeatureSource::FeatureAvailability::NoFeaturesAvailable;
2786+
else
2787+
return QgsFeatureSource::FeatureAvailability::FeaturesAvailable;
2788+
}
2789+
27692790
bool QgsVectorLayer::commitChanges()
27702791
{
27712792
mCommitErrors.clear();

‎src/core/qgsvectorlayer.h

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -930,6 +930,18 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
930930
*/
931931
long featureCount( const QString &legendKey ) const;
932932

933+
/**
934+
* Determines if this vector layer has features.
935+
*
936+
* \warning when a layer is editable and some features
937+
* have been deleted, this will return
938+
* QgsFeatureSource::FeatureAvailability::FeaturesMayBeAvailable
939+
* to avoid a potentially expensive call to the dataprovider.
940+
*
941+
* \since QGIS 3.4
942+
*/
943+
FeatureAvailability hasFeatures() const override;
944+
933945
/**
934946
* Update the data source of the layer. The layer's renderer and legend will be preserved only
935947
* if the geometry type of the new data source matches the current geometry type of the layer.

‎src/providers/postgres/qgspostgresprovider.cpp

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3190,6 +3190,19 @@ long QgsPostgresProvider::featureCount() const
31903190
return num;
31913191
}
31923192

3193+
bool QgsPostgresProvider::empty() const
3194+
{
3195+
QString sql = QStringLiteral( "SELECT EXISTS (SELECT * FROM %1%2 LIMIT 1)" ).arg( mQuery, filterWhereClause() );
3196+
QgsPostgresResult res( connectionRO()->PQexec( sql ) );
3197+
if ( res.PQresultStatus() != PGRES_TUPLES_OK )
3198+
{
3199+
pushError( res.PQresultErrorMessage() );
3200+
return false;
3201+
}
3202+
3203+
return res.PQgetvalue( 0, 0 ) != 't';
3204+
}
3205+
31933206
QgsRectangle QgsPostgresProvider::extent() const
31943207
{
31953208
if ( mGeometryColumn.isNull() )

‎src/providers/postgres/qgspostgresprovider.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,16 @@ class QgsPostgresProvider : public QgsVectorDataProvider
109109

110110
long featureCount() const override;
111111

112+
/**
113+
* Determines if there is at least one feature available on this table.
114+
*
115+
* \note In contrast to the featureCount() method, this method is not
116+
* affected by estimated metadata.
117+
*
118+
* \since QGIS 3.4
119+
*/
120+
bool empty() const override;
121+
112122
/**
113123
* Returns a string representation of the endian-ness for the layer
114124
*/

‎src/providers/wfs/qgswfsprovider.cpp

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1206,6 +1206,27 @@ QString QgsWFSProvider::translateMetadataValue( const QString &mdKey, const QVar
12061206
{
12071207
return value.toString();
12081208
}
1209+
}
1210+
1211+
bool QgsWFSProvider::empty() const
1212+
{
1213+
QgsFeature f;
1214+
QgsFeatureRequest request;
1215+
request.setSubsetOfAttributes( QgsAttributeList() );
1216+
request.setFlags( QgsFeatureRequest::NoGeometry );
1217+
1218+
// Whoops, the WFS provider returns an empty iterator when we are using
1219+
// a setLimit call in combination with a subsetString.
1220+
// Remove this method (and default to the QgsVectorDataProvider one)
1221+
// once this is fixed
1222+
#if 0
1223+
request.setLimit( 1 );
1224+
#endif
1225+
if ( getFeatures( request ).nextFeature( f ) )
1226+
return false;
1227+
else
1228+
return true;
1229+
12091230
};
12101231

12111232
bool QgsWFSProvider::describeFeatureType( QString &geometryAttribute, QgsFields &fields, QgsWkbTypes::Type &geomType )

‎src/providers/wfs/qgswfsprovider.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,8 @@ class QgsWFSProvider : public QgsVectorDataProvider
111111
QString translateMetadataKey( const QString &mdKey ) const override;
112112
QString translateMetadataValue( const QString &mdKey, const QVariant &value ) const override;
113113

114+
bool empty() const override;
115+
114116
public slots:
115117

116118
void reloadData() override;

‎tests/src/python/providertestbase.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
QgsVectorLayerFeatureSource,
2929
QgsFeatureSink,
3030
QgsTestUtils,
31+
QgsFeatureSource,
3132
NULL
3233
)
3334
from qgis.PyQt.QtTest import QSignalSpy
@@ -422,6 +423,46 @@ def testFeatureCount(self):
422423
self.assertEqual(count, 0)
423424
self.assertEqual(self.source.featureCount(), 5)
424425

426+
def testEmpty(self):
427+
self.assertFalse(self.source.empty())
428+
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
429+
430+
if self.source.supportsSubsetString():
431+
try:
432+
backup = self.source.subsetString()
433+
# Add a subset string and test feature count
434+
subset = self.getSubsetString()
435+
self.source.setSubsetString(subset)
436+
self.assertFalse(self.source.empty())
437+
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
438+
subsetNoMatching = self.getSubsetStringNoMatching()
439+
self.source.setSubsetString(subsetNoMatching)
440+
self.assertTrue(self.source.empty())
441+
self.assertEqual(self.source.hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
442+
finally:
443+
self.source.setSubsetString(None)
444+
self.assertFalse(self.source.empty())
445+
446+
# If the provider supports tests on editable layers
447+
if getattr(self, 'getEditableLayer', None):
448+
l = self.getEditableLayer()
449+
self.assertTrue(l.isValid())
450+
451+
self.assertEqual(l.hasFeatures(), QgsFeatureSource.FeaturesAvailable)
452+
453+
# Test that deleting some features in the edit buffer does not
454+
# return empty, we accept FeaturesAvailable as well as
455+
# MaybeAvailable
456+
l.startEditing()
457+
l.deleteFeature(next(l.getFeatures()).id())
458+
self.assertNotEqual(l.hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
459+
l.rollBack()
460+
461+
# Call truncate(), we need an empty set now
462+
l.dataProvider().truncate()
463+
self.assertTrue(l.dataProvider().empty())
464+
self.assertEqual(l.dataProvider().hasFeatures(), QgsFeatureSource.NoFeaturesAvailable)
465+
425466
def testGetFeaturesNoGeometry(self):
426467
""" Test that no geometry is present when fetching features without geometry"""
427468

0 commit comments

Comments
 (0)
Please sign in to comment.