Skip to content

Commit ba3d9ed

Browse files
committed
Cache unique values when creating features
Fixes #21305 - pasting features is very slow Aggressively optimize createFeature for speed and introduces createFeatures for bulk creation.
1 parent f2e745e commit ba3d9ed

File tree

5 files changed

+231
-77
lines changed

5 files changed

+231
-77
lines changed

python/core/auto_generated/qgsvectorlayerutils.sip.in

+45
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,40 @@ Returns the duplicated features in the given layer
5757

5858
};
5959

60+
class QgsFeaturesData
61+
{
62+
%Docstring
63+
Encapsulate geometry and attributes for new features, to be passed to createFeatures
64+
65+
.. seealso:: :py:func:`createFeatures`
66+
67+
.. versionadded:: 3.6
68+
%End
69+
70+
%TypeHeaderCode
71+
#include "qgsvectorlayerutils.h"
72+
%End
73+
public:
74+
75+
QgsFeaturesData( const QgsGeometry &geometry = QgsGeometry(), const QgsAttributeMap &attributes = QgsAttributeMap() );
76+
%Docstring
77+
Constructs a new QgsFeaturesData with given ``geometry`` and ``attributes``
78+
%End
79+
80+
QgsGeometry geometry() const;
81+
%Docstring
82+
Returns geometry
83+
%End
84+
85+
QgsAttributeMap attributes() const;
86+
%Docstring
87+
Returns attributes
88+
%End
89+
90+
};
91+
92+
typedef QList<QgsVectorLayerUtils::QgsFeaturesData> QgsFeaturesDataList;
93+
6094
static QgsFeatureIterator getValuesIterator( const QgsVectorLayer *layer, const QString &fieldOrExpression, bool &ok, bool selectedOnly );
6195
%Docstring
6296
Create a feature iterator for a specified field name or expression.
@@ -143,6 +177,17 @@ Creates a new feature ready for insertion into a layer. Default values and const
143177
passed for the new feature to copy as many attribute values as possible from the map,
144178
assuming that they respect the layer's constraints. Note that the created feature is not
145179
automatically inserted into the layer.
180+
%End
181+
182+
static QgsFeatureList createFeatures( const QgsVectorLayer *layer,
183+
const QgsFeaturesDataList &featuresData,
184+
QgsExpressionContext *context = 0 );
185+
%Docstring
186+
Creates a set of new features ready for insertion into a layer. Default values and constraints
187+
(e.g., unique constraints) will automatically be handled. An optional attribute map can be
188+
passed for the new feature to copy as many attribute values as possible from the map,
189+
assuming that they respect the layer's constraints. Note that the created features are not
190+
automatically inserted into the layer.
146191
%End
147192

148193
static QgsFeature duplicateFeature( QgsVectorLayer *layer, const QgsFeature &feature, QgsProject *project, int depth, QgsDuplicateFeatureContext &duplicateFeatureContext /Out/ );

src/app/qgisapp.cpp

+6-4
Original file line numberDiff line numberDiff line change
@@ -8996,11 +8996,12 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer )
89968996
QgsExpressionContext context = pasteVectorLayer->createExpressionContext();
89978997

89988998
QgsFeatureList compatibleFeatures( QgsVectorLayerUtils::makeFeaturesCompatible( features, pasteVectorLayer ) );
8999-
QgsFeatureList newFeatures;
8999+
QgsVectorLayerUtils::QgsFeaturesDataList newFeaturesDataList;
9000+
newFeaturesDataList.reserve( compatibleFeatures.size() );
9001+
90009002
// Count collapsed geometries
90019003
int invalidGeometriesCount = 0;
90029004

9003-
newFeatures.reserve( compatibleFeatures.size() );
90049005
for ( const auto &feature : qgis::as_const( compatibleFeatures ) )
90059006
{
90069007

@@ -9022,8 +9023,10 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer )
90229023
}
90239024
// now create new feature using pasted feature as a template. This automatically handles default
90249025
// values and field constraints
9025-
newFeatures << QgsVectorLayerUtils::createFeature( pasteVectorLayer, geom, attrMap, &context );
9026+
newFeaturesDataList << QgsVectorLayerUtils::QgsFeaturesData( geom, attrMap );
90269027
}
9028+
9029+
QgsFeatureList newFeatures {QgsVectorLayerUtils::createFeatures( pasteVectorLayer, newFeaturesDataList, &context )};
90279030
pasteVectorLayer->addFeatures( newFeatures );
90289031
QgsFeatureIds newIds;
90299032
newIds.reserve( newFeatures.size() );
@@ -9032,7 +9035,6 @@ void QgisApp::pasteFromClipboard( QgsMapLayer *destinationLayer )
90329035
newIds << f.id();
90339036
}
90349037

9035-
90369038
pasteVectorLayer->selectByIds( newIds );
90379039
pasteVectorLayer->endEditCommand();
90389040
pasteVectorLayer->updateExtents();

src/core/qgsvectorlayerutils.cpp

+104-73
Original file line numberDiff line numberDiff line change
@@ -358,11 +358,17 @@ bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const
358358

359359
QgsFeature QgsVectorLayerUtils::createFeature( const QgsVectorLayer *layer, const QgsGeometry &geometry,
360360
const QgsAttributeMap &attributes, QgsExpressionContext *context )
361+
{
362+
return createFeatures( layer, QgsFeaturesDataList() << QgsFeaturesData( geometry, attributes ), context ).first();
363+
}
364+
365+
QgsFeatureList QgsVectorLayerUtils::createFeatures( const QgsVectorLayer *layer, const QgsFeaturesDataList &featuresData, QgsExpressionContext *context )
361366
{
362367
if ( !layer )
363-
{
364-
return QgsFeature();
365-
}
368+
return QgsFeatureList();
369+
370+
QgsFeatureList result;
371+
result.reserve( featuresData.length() );
366372

367373
QgsExpressionContext *evalContext = context;
368374
std::unique_ptr< QgsExpressionContext > tempContext;
@@ -375,94 +381,104 @@ QgsFeature QgsVectorLayerUtils::createFeature( const QgsVectorLayer *layer, cons
375381

376382
QgsFields fields = layer->fields();
377383

378-
QgsFeature newFeature( fields );
379-
newFeature.setValid( true );
380-
newFeature.setGeometry( geometry );
384+
// Cache unique values
385+
QMap<int, QSet<QVariant>> uniqueValueCaches;
381386

382-
// initialize attributes
383-
newFeature.initAttributes( fields.count() );
384-
for ( int idx = 0; idx < fields.count(); ++idx )
387+
for ( const auto &fd : qgis::as_const( featuresData ) )
385388
{
386-
QVariant v;
387-
bool checkUnique = true;
388389

389-
// in order of priority:
390-
// 1. passed attribute value and if field does not have a unique constraint like primary key
391-
if ( attributes.contains( idx ) )
390+
QgsFeature newFeature( fields );
391+
newFeature.setValid( true );
392+
newFeature.setGeometry( fd.geometry() );
393+
394+
// initialize attributes
395+
newFeature.initAttributes( fields.count() );
396+
for ( int idx = 0; idx < fields.count(); ++idx )
392397
{
393-
v = attributes.value( idx );
394-
}
398+
QVariant v;
399+
bool checkUnique = true;
400+
const bool hasUniqueConstraint { static_cast<bool>( fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique ) };
395401

396-
// Cache unique values
397-
QSet<QVariant> uniqueValues { layer->uniqueValues( idx ) };
402+
// in order of priority:
403+
// 1. passed attribute value and if field does not have a unique constraint like primary key
404+
if ( fd.attributes().contains( idx ) )
405+
{
406+
v = fd.attributes().value( idx );
407+
}
398408

399-
// 2. client side default expression
400-
// note - deliberately not using else if!
401-
if ( ( !v.isValid() || ( fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique
402-
&& uniqueValues.contains( v ) ) )
403-
&& layer->defaultValueDefinition( idx ).isValid() )
404-
{
405-
// client side default expression set - takes precedence over all. Why? Well, this is the only default
406-
// which QGIS users have control over, so we assume that they're deliberately overriding any
407-
// provider defaults for some good reason and we should respect that
408-
v = layer->defaultValue( idx, newFeature, evalContext );
409-
}
409+
// Cache unique values
410+
if ( hasUniqueConstraint && ! uniqueValueCaches.contains( idx ) )
411+
uniqueValueCaches[ idx ] = layer->uniqueValues( idx );
410412

411-
// 3. provider side default value clause
412-
// note - not an else if deliberately. Users may return null from a default value expression to fallback to provider defaults
413-
if ( ( !v.isValid() || ( fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique
414-
&& uniqueValues.contains( v ) ) )
415-
&& fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
416-
{
417-
int providerIndex = fields.fieldOriginIndex( idx );
418-
QString providerDefault = layer->dataProvider()->defaultValueClause( providerIndex );
419-
if ( !providerDefault.isEmpty() )
413+
// 2. client side default expression
414+
// note - deliberately not using else if!
415+
if ( ( !v.isValid() || ( hasUniqueConstraint
416+
&& uniqueValueCaches[ idx ].contains( v ) ) )
417+
&& layer->defaultValueDefinition( idx ).isValid() )
420418
{
421-
v = providerDefault;
422-
checkUnique = false;
419+
// client side default expression set - takes precedence over all. Why? Well, this is the only default
420+
// which QGIS users have control over, so we assume that they're deliberately overriding any
421+
// provider defaults for some good reason and we should respect that
422+
v = layer->defaultValue( idx, newFeature, evalContext );
423423
}
424-
}
425424

426-
// 4. provider side default literal
427-
// note - deliberately not using else if!
428-
if ( ( !v.isValid() || ( checkUnique && fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique
429-
&& uniqueValues.contains( v ) ) )
430-
&& fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
431-
{
432-
int providerIndex = fields.fieldOriginIndex( idx );
433-
v = layer->dataProvider()->defaultValue( providerIndex );
434-
if ( v.isValid() )
425+
// 3. provider side default value clause
426+
// note - not an else if deliberately. Users may return null from a default value expression to fallback to provider defaults
427+
if ( ( !v.isValid() || ( hasUniqueConstraint
428+
&& uniqueValueCaches[ idx ].contains( v ) ) )
429+
&& fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
435430
{
436-
//trust that the provider default has been sensibly set not to violate any constraints
437-
checkUnique = false;
431+
int providerIndex = fields.fieldOriginIndex( idx );
432+
QString providerDefault = layer->dataProvider()->defaultValueClause( providerIndex );
433+
if ( !providerDefault.isEmpty() )
434+
{
435+
v = providerDefault;
436+
checkUnique = false;
437+
}
438438
}
439-
}
440439

441-
// 5. passed attribute value
442-
// note - deliberately not using else if!
443-
if ( !v.isValid() && attributes.contains( idx ) )
444-
{
445-
v = attributes.value( idx );
446-
}
440+
// 4. provider side default literal
441+
// note - deliberately not using else if!
442+
if ( ( !v.isValid() || ( checkUnique && hasUniqueConstraint
443+
&& uniqueValueCaches[ idx ].contains( v ) ) )
444+
&& fields.fieldOrigin( idx ) == QgsFields::OriginProvider )
445+
{
446+
int providerIndex = fields.fieldOriginIndex( idx );
447+
v = layer->dataProvider()->defaultValue( providerIndex );
448+
if ( v.isValid() )
449+
{
450+
//trust that the provider default has been sensibly set not to violate any constraints
451+
checkUnique = false;
452+
}
453+
}
447454

448-
// last of all... check that unique constraints are respected
449-
// we can't handle not null or expression constraints here, since there's no way to pick a sensible
450-
// value if the constraint is violated
451-
if ( checkUnique && fields.at( idx ).constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
452-
{
453-
if ( uniqueValues.contains( v ) )
455+
// 5. passed attribute value
456+
// note - deliberately not using else if!
457+
if ( !v.isValid() && fd.attributes().contains( idx ) )
454458
{
455-
// unique constraint violated
456-
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValue( layer, idx, v );
457-
if ( uniqueValue.isValid() )
458-
v = uniqueValue;
459+
v = fd.attributes().value( idx );
459460
}
460-
}
461461

462-
newFeature.setAttribute( idx, v );
462+
// last of all... check that unique constraints are respected
463+
// we can't handle not null or expression constraints here, since there's no way to pick a sensible
464+
// value if the constraint is violated
465+
if ( checkUnique && hasUniqueConstraint )
466+
{
467+
if ( uniqueValueCaches[ idx ].contains( v ) )
468+
{
469+
// unique constraint violated
470+
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValue( layer, idx, v );
471+
if ( uniqueValue.isValid() )
472+
v = uniqueValue;
473+
}
474+
}
475+
if ( hasUniqueConstraint )
476+
uniqueValueCaches[ idx ].insert( v );
477+
newFeature.setAttribute( idx, v );
478+
}
479+
result.append( newFeature );
463480
}
464-
465-
return newFeature;
481+
return result;
466482
}
467483

468484
QgsFeature QgsVectorLayerUtils::duplicateFeature( QgsVectorLayer *layer, const QgsFeature &feature, QgsProject *project, int depth, QgsDuplicateFeatureContext &duplicateFeatureContext )
@@ -772,3 +788,18 @@ QMap<QgsVectorLayer *, QgsFeatureIds> QgsVectorLayerUtils::QgsDuplicateFeatureC
772788
return mDuplicatedFeatures;
773789
}
774790
*/
791+
792+
QgsVectorLayerUtils::QgsFeaturesData::QgsFeaturesData( const QgsGeometry &geometry, const QgsAttributeMap &attributes ):
793+
mGeometry( geometry ),
794+
mAttributes( attributes )
795+
{}
796+
797+
QgsGeometry QgsVectorLayerUtils::QgsFeaturesData::geometry() const
798+
{
799+
return mGeometry;
800+
}
801+
802+
QgsAttributeMap QgsVectorLayerUtils::QgsFeaturesData::attributes() const
803+
{
804+
return mAttributes;
805+
}

src/core/qgsvectorlayerutils.h

+42
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,37 @@ class CORE_EXPORT QgsVectorLayerUtils
7070
void setDuplicatedFeatures( QgsVectorLayer *layer, const QgsFeatureIds &ids );
7171
};
7272

73+
/**
74+
* \ingroup core
75+
* \class QgsFeatureSetData
76+
* \brief Encapsulate geometry and attributes for new features, to be passed to createFeatures
77+
* \see createFeatures()
78+
* \since QGIS 3.6
79+
*/
80+
class CORE_EXPORT QgsFeaturesData
81+
{
82+
public:
83+
84+
/**
85+
* Constructs a new QgsFeaturesData with given \a geometry and \a attributes
86+
*/
87+
QgsFeaturesData( const QgsGeometry &geometry = QgsGeometry(), const QgsAttributeMap &attributes = QgsAttributeMap() );
88+
89+
//! Returns geometry
90+
QgsGeometry geometry() const;
91+
92+
//! Returns attributes
93+
QgsAttributeMap attributes() const;
94+
95+
private:
96+
QgsGeometry mGeometry;
97+
QgsAttributeMap mAttributes;
98+
};
99+
100+
// SIP does not lile "using", use legacy typedef
101+
//! Alias for list of QgsFeaturesData
102+
typedef QList<QgsVectorLayerUtils::QgsFeaturesData> QgsFeaturesDataList;
103+
73104
/**
74105
* Create a feature iterator for a specified field name or expression.
75106
* \param layer vector layer to retrieve values from
@@ -145,6 +176,17 @@ class CORE_EXPORT QgsVectorLayerUtils
145176
const QgsAttributeMap &attributes = QgsAttributeMap(),
146177
QgsExpressionContext *context = nullptr );
147178

179+
/**
180+
* Creates a set of new features ready for insertion into a layer. Default values and constraints
181+
* (e.g., unique constraints) will automatically be handled. An optional attribute map can be
182+
* passed for the new feature to copy as many attribute values as possible from the map,
183+
* assuming that they respect the layer's constraints. Note that the created features are not
184+
* automatically inserted into the layer.
185+
*/
186+
static QgsFeatureList createFeatures( const QgsVectorLayer *layer,
187+
const QgsFeaturesDataList &featuresData,
188+
QgsExpressionContext *context = nullptr );
189+
148190
/**
149191
* Duplicates a feature and it's children (one level deep). It calls CreateFeature, so
150192
* default values and constraints (e.g., unique constraints) will automatically be handled.

0 commit comments

Comments
 (0)