Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
Fix unique values when generating a set of features
  • Loading branch information
elpaso committed Feb 19, 2019
1 parent 11c9ce0 commit d239ea2
Show file tree
Hide file tree
Showing 4 changed files with 117 additions and 2 deletions.
9 changes: 9 additions & 0 deletions python/core/auto_generated/qgsvectorlayerutils.sip.in
Expand Up @@ -156,6 +156,15 @@ Returns a new attribute value for the specified field index which is guaranteed
value can be used as a basis for generated values.

.. seealso:: :py:func:`valueExists`
%End

static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );
%Docstring
Returns a new attribute value for the specified field index which is guaranteed to
be unique within regard to ``existingValues``.
The optional seed value can be used as a basis for generated values.

.. versionadded:: 3.6
%End

static bool validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors /Out/,
Expand Down
83 changes: 82 additions & 1 deletion src/core/qgsvectorlayerutils.cpp
Expand Up @@ -264,6 +264,87 @@ QVariant QgsVectorLayerUtils::createUniqueValue( const QgsVectorLayer *layer, in
return QVariant();
}

QVariant QgsVectorLayerUtils::createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed )
{
if ( !layer )
return QVariant();

QgsFields fields = layer->fields();

if ( fieldIndex < 0 || fieldIndex >= fields.count() )
return QVariant();

QgsField field = fields.at( fieldIndex );

if ( field.isNumeric() )
{
QVariant maxVal = existingValues.isEmpty() ? 0 : *std::max_element( existingValues.begin(), existingValues.end() );
QVariant newVar( maxVal.toLongLong() + 1 );
if ( field.convertCompatible( newVar ) )
return newVar;
else
return QVariant();
}
else
{
switch ( field.type() )
{
case QVariant::String:
{
QString base;
if ( seed.isValid() )
base = seed.toString();

if ( !base.isEmpty() )
{
// strip any existing _1, _2 from the seed
QRegularExpression rx( QStringLiteral( "(.*)_\\d+" ) );
QRegularExpressionMatch match = rx.match( base );
if ( match.hasMatch() )
{
base = match.captured( 1 );
}
}
else
{
// no base seed - fetch first value from layer
QgsFeatureRequest req;
base = existingValues.isEmpty() ? QString() : existingValues.values().first().toString();
}

// try variants like base_1, base_2, etc until a new value found
QStringList vals;
for ( const auto &v : qgis::as_const( existingValues ) )
{
if ( v.toString().startsWith( base ) )
vals.push_back( v.toString() );
}

// might already be unique
if ( !base.isEmpty() && !vals.contains( base ) )
return base;

for ( int i = 1; i < 10000; ++i )
{
QString testVal = base + '_' + QString::number( i );
if ( !vals.contains( testVal ) )
return testVal;
}

// failed
return QVariant();
}

default:
// todo other types - dates? times?
break;
}
}

return QVariant();

}

bool QgsVectorLayerUtils::validateAttribute( const QgsVectorLayer *layer, const QgsFeature &feature, int attributeIndex, QStringList &errors,
QgsFieldConstraints::ConstraintStrength strength, QgsFieldConstraints::ConstraintOrigin origin )
{
Expand Down Expand Up @@ -468,7 +549,7 @@ QgsFeatureList QgsVectorLayerUtils::createFeatures( const QgsVectorLayer *layer,
if ( uniqueValueCaches[ idx ].contains( v ) )
{
// unique constraint violated
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValue( layer, idx, v );
QVariant uniqueValue = QgsVectorLayerUtils::createUniqueValueFromCache( layer, idx, uniqueValueCaches[ idx ], v );
if ( uniqueValue.isValid() )
v = uniqueValue;
}
Expand Down
8 changes: 8 additions & 0 deletions src/core/qgsvectorlayerutils.h
Expand Up @@ -155,6 +155,14 @@ class CORE_EXPORT QgsVectorLayerUtils
*/
static QVariant createUniqueValue( const QgsVectorLayer *layer, int fieldIndex, const QVariant &seed = QVariant() );

/**
* Returns a new attribute value for the specified field index which is guaranteed to
* be unique within regard to \a existingValues.
* The optional seed value can be used as a basis for generated values.
* \since QGIS 3.6
*/
static QVariant createUniqueValueFromCache( const QgsVectorLayer *layer, int fieldIndex, const QSet<QVariant> &existingValues, const QVariant &seed = QVariant() );

/**
* Tests an attribute value to check whether it passes all constraints which are present on the corresponding field.
* Returns true if the attribute value is valid for the field. Any constraint failures will be reported in the errors argument.
Expand Down
19 changes: 18 additions & 1 deletion tests/src/python/test_qgsvectorlayerutils.py
Expand Up @@ -457,7 +457,7 @@ def testDuplicateFeature(self):
print( "\nFeatures on layer2 (after duplication)")
for f in layer2.getFeatures():
print( f.attributes() )
print( "\nAll Features and relations")
featit=layer1.getFeatures()
f=QgsFeature()
Expand Down Expand Up @@ -570,6 +570,23 @@ def test_make_features_compatible_attributes(self):
self.assertEqual(f1.attributes()[3], 'blah')
self.assertEqual(f1.attributes()[4], 'blergh')

def test_create_multiple_unique_constraint(self):
"""Test create multiple features with unique constraint"""

vl = createLayerWithOnePoint()

# field expression check
vl.setFieldConstraint(1, QgsFieldConstraints.ConstraintUnique)

features_data = []
context = vl.createExpressionContext()
for i in range(2):
features_data.append(QgsVectorLayerUtils.QgsFeatureData(QgsGeometry.fromWkt('Point (7 44)'), {0: 'test_%s' % i, 1: 123}))
features = QgsVectorLayerUtils.createFeatures(vl, features_data, context)

self.assertEqual(features[0].attributes()[1], 124)
self.assertEqual(features[1].attributes()[1], 125)


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

0 comments on commit d239ea2

Please sign in to comment.