Skip to content

Commit d239ea2

Browse files
committed
Fix unique values when generating a set of features
1 parent 11c9ce0 commit d239ea2

File tree

4 files changed

+117
-2
lines changed

4 files changed

+117
-2
lines changed

python/core/auto_generated/qgsvectorlayerutils.sip.in

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,15 @@ Returns a new attribute value for the specified field index which is guaranteed
156156
value can be used as a basis for generated values.
157157

158158
.. seealso:: :py:func:`valueExists`
159+
%End
160+
161+
static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );
162+
%Docstring
163+
Returns a new attribute value for the specified field index which is guaranteed to
164+
be unique within regard to ``existingValues``.
165+
The optional seed value can be used as a basis for generated values.
166+
167+
.. versionadded:: 3.6
159168
%End
160169

161170
static bool validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors /Out/,

src/core/qgsvectorlayerutils.cpp

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,6 +264,87 @@ QVariant QgsVectorLayerUtils::createUniqueValue( const QgsVectorLayer *layer, in
264264
return QVariant();
265265
}
266266

267+
QVariant QgsVectorLayerUtils::createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed )
268+
{
269+
if ( !layer )
270+
return QVariant();
271+
272+
QgsFields fields = layer->fields();
273+
274+
if ( fieldIndex < 0 || fieldIndex >= fields.count() )
275+
return QVariant();
276+
277+
QgsField field = fields.at( fieldIndex );
278+
279+
if ( field.isNumeric() )
280+
{
281+
QVariant maxVal = existingValues.isEmpty() ? 0 : *std::max_element( existingValues.begin(), existingValues.end() );
282+
QVariant newVar( maxVal.toLongLong() + 1 );
283+
if ( field.convertCompatible( newVar ) )
284+
return newVar;
285+
else
286+
return QVariant();
287+
}
288+
else
289+
{
290+
switch ( field.type() )
291+
{
292+
case QVariant::String:
293+
{
294+
QString base;
295+
if ( seed.isValid() )
296+
base = seed.toString();
297+
298+
if ( !base.isEmpty() )
299+
{
300+
// strip any existing _1, _2 from the seed
301+
QRegularExpression rx( QStringLiteral( "(.*)_\\d+" ) );
302+
QRegularExpressionMatch match = rx.match( base );
303+
if ( match.hasMatch() )
304+
{
305+
base = match.captured( 1 );
306+
}
307+
}
308+
else
309+
{
310+
// no base seed - fetch first value from layer
311+
QgsFeatureRequest req;
312+
base = existingValues.isEmpty() ? QString() : existingValues.values().first().toString();
313+
}
314+
315+
// try variants like base_1, base_2, etc until a new value found
316+
QStringList vals;
317+
for ( const auto &v : qgis::as_const( existingValues ) )
318+
{
319+
if ( v.toString().startsWith( base ) )
320+
vals.push_back( v.toString() );
321+
}
322+
323+
// might already be unique
324+
if ( !base.isEmpty() && !vals.contains( base ) )
325+
return base;
326+
327+
for ( int i = 1; i < 10000; ++i )
328+
{
329+
QString testVal = base + '_' + QString::number( i );
330+
if ( !vals.contains( testVal ) )
331+
return testVal;
332+
}
333+
334+
// failed
335+
return QVariant();
336+
}
337+
338+
default:
339+
// todo other types - dates? times?
340+
break;
341+
}
342+
}
343+
344+
return QVariant();
345+
346+
}
347+
267348
bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors,
268349
QgsFieldConstraints::ConstraintStrength strength, QgsFieldConstraints::ConstraintOrigin origin )
269350
{
@@ -468,7 +549,7 @@ QgsFeatureList QgsVectorLayerUtils::createFeatures( const QgsVectorLayer *layer,
468549
if ( uniqueValueCaches[ idx ].contains( v ) )
469550
{
470551
// unique constraint violated
471-
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValue( layer, idx, v );
552+
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValueFromCache( layer, idx, uniqueValueCaches[ idx ], v );
472553
if ( uniqueValue.isValid() )
473554
v = uniqueValue;
474555
}

src/core/qgsvectorlayerutils.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,14 @@ class CORE_EXPORT QgsVectorLayerUtils
155155
*/
156156
static QVariant createUniqueValue( const QgsVectorLayer *layer, int fieldIndex, const QVariant &seed = QVariant() );
157157

158+
/**
159+
* Returns a new attribute value for the specified field index which is guaranteed to
160+
* be unique within regard to \a existingValues.
161+
* The optional seed value can be used as a basis for generated values.
162+
* \since QGIS 3.6
163+
*/
164+
static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );
165+
158166
/**
159167
* Tests an attribute value to check whether it passes all constraints which are present on the corresponding field.
160168
* Returns true if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.

tests/src/python/test_qgsvectorlayerutils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -457,7 +457,7 @@ def testDuplicateFeature(self):
457457
print( "\nFeatures on layer2 (after duplication)")
458458
for f in layer2.getFeatures():
459459
print( f.attributes() )
460-
460+
461461
print( "\nAll Features and relations")
462462
featit=layer1.getFeatures()
463463
f=QgsFeature()
@@ -570,6 +570,23 @@ def test_make_features_compatible_attributes(self):
570570
self.assertEqual(f1.attributes()[3], 'blah')
571571
self.assertEqual(f1.attributes()[4], 'blergh')
572572

573+
def test_create_multiple_unique_constraint(self):
574+
"""Test create multiple features with unique constraint"""
575+
576+
vl = createLayerWithOnePoint()
577+
578+
# field expression check
579+
vl.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique)
580+
581+
features_data = []
582+
context = vl.createExpressionContext()
583+
for i in range(2):
584+
features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 44)'), {0: 'test_%s' % i, 1: 123}))
585+
features = QgsVectorLayerUtils.createFeatures(vl, features_data, context)
586+
587+
self.assertEqual(features[0].attributes()[1], 124)
588+
self.assertEqual(features[1].attributes()[1], 125)
589+
573590

574591
if __name__ == '__main__':
575592
unittest.main()

0 commit comments

Comments
 (0)