Skip to content

Commit a376867

Browse files
committedJun 10, 2016
Correctly support joins using virtual fields (fix #14820)
1 parent df0d596 commit a376867

File tree

3 files changed

+232
-103
lines changed

3 files changed

+232
-103
lines changed
 

‎src/core/qgsvectorlayerfeatureiterator.cpp

Lines changed: 124 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -93,11 +93,7 @@ QgsVectorLayerFeatureIterator::QgsVectorLayerFeatureIterator( QgsVectorLayerFeat
9393
, mFetchedFid( false )
9494
, mInterruptionChecker( nullptr )
9595
{
96-
prepareExpressions();
97-
98-
// prepare joins: may add more attributes to fetch (in order to allow join)
99-
if ( mSource->mJoinBuffer->containsJoins() )
100-
prepareJoins();
96+
prepareFields();
10197

10298
mHasVirtualAttributes = !mFetchJoinInfo.isEmpty() || !mExpressionFieldInfo.isEmpty();
10399

@@ -461,128 +457,132 @@ void QgsVectorLayerFeatureIterator::rewindEditBuffer()
461457
mFetchChangedGeomIt = mSource->mChangedGeometries.constBegin();
462458
}
463459

460+
void QgsVectorLayerFeatureIterator::prepareJoin( int fieldIdx )
461+
{
462+
if ( !mSource->mFields.exists( fieldIdx ) )
463+
return;
464464

465+
if ( mSource->mFields.fieldOrigin( fieldIdx ) != QgsFields::OriginJoin )
466+
return;
465467

466-
void QgsVectorLayerFeatureIterator::prepareJoins()
467-
{
468-
QgsAttributeList fetchAttributes = ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes ) ? mRequest.subsetOfAttributes() : mSource->mFields.allAttributesList();
469-
QgsAttributeList sourceJoinFields; // attributes that also need to be fetched from this layer in order to have joins working
468+
int sourceLayerIndex;
469+
const QgsVectorJoinInfo* joinInfo = mSource->mJoinBuffer->joinForFieldIndex( fieldIdx, mSource->mFields, sourceLayerIndex );
470+
Q_ASSERT( joinInfo );
470471

471-
mFetchJoinInfo.clear();
472+
QgsVectorLayer* joinLayer = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( joinInfo->joinLayerId ) );
473+
Q_ASSERT( joinLayer );
472474

473-
for ( QgsAttributeList::const_iterator attIt = fetchAttributes.constBegin(); attIt != fetchAttributes.constEnd(); ++attIt )
475+
if ( !mFetchJoinInfo.contains( joinInfo ) )
474476
{
475-
if ( !mSource->mFields.exists( *attIt ) )
476-
continue;
477+
FetchJoinInfo info;
478+
info.joinInfo = joinInfo;
479+
info.joinLayer = joinLayer;
480+
info.indexOffset = mSource->mJoinBuffer->joinedFieldsOffset( joinInfo, mSource->mFields );
477481

478-
if ( mSource->mFields.fieldOrigin( *attIt ) != QgsFields::OriginJoin )
479-
continue;
482+
if ( joinInfo->targetFieldName.isEmpty() )
483+
info.targetField = joinInfo->targetFieldIndex; //for compatibility with 1.x
484+
else
485+
info.targetField = mSource->mFields.indexFromName( joinInfo->targetFieldName );
480486

481-
int sourceLayerIndex;
482-
const QgsVectorJoinInfo* joinInfo = mSource->mJoinBuffer->joinForFieldIndex( *attIt, mSource->mFields, sourceLayerIndex );
483-
Q_ASSERT( joinInfo );
487+
if ( joinInfo->joinFieldName.isEmpty() )
488+
info.joinField = joinInfo->joinFieldIndex; //for compatibility with 1.x
489+
else
490+
info.joinField = joinLayer->fields().indexFromName( joinInfo->joinFieldName );
484491

485-
QgsVectorLayer* joinLayer = qobject_cast<QgsVectorLayer*>( QgsMapLayerRegistry::instance()->mapLayer( joinInfo->joinLayerId ) );
486-
Q_ASSERT( joinLayer );
492+
// for joined fields, we always need to request the targetField from the provider too
493+
if ( !mPreparedFields.contains( info.targetField ) && !mFieldsToPrepare.contains( info.targetField ) )
494+
mFieldsToPrepare << info.targetField;
487495

488-
if ( !mFetchJoinInfo.contains( joinInfo ) )
489-
{
490-
FetchJoinInfo info;
491-
info.joinInfo = joinInfo;
492-
info.joinLayer = joinLayer;
493-
info.indexOffset = mSource->mJoinBuffer->joinedFieldsOffset( joinInfo, mSource->mFields );
494-
495-
if ( joinInfo->targetFieldName.isEmpty() )
496-
info.targetField = joinInfo->targetFieldIndex; //for compatibility with 1.x
497-
else
498-
info.targetField = mSource->mFields.indexFromName( joinInfo->targetFieldName );
499-
500-
if ( joinInfo->joinFieldName.isEmpty() )
501-
info.joinField = joinInfo->joinFieldIndex; //for compatibility with 1.x
502-
else
503-
info.joinField = joinLayer->fields().indexFromName( joinInfo->joinFieldName );
504-
505-
// for joined fields, we always need to request the targetField from the provider too
506-
if ( !fetchAttributes.contains( info.targetField ) )
507-
sourceJoinFields << info.targetField;
508-
509-
mFetchJoinInfo.insert( joinInfo, info );
510-
}
496+
if ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes && !mRequest.subsetOfAttributes().contains( info.targetField ) )
497+
mRequest.setSubsetOfAttributes( mRequest.subsetOfAttributes() << info.targetField );
511498

512-
// store field source index - we'll need it when fetching from provider
513-
mFetchJoinInfo[ joinInfo ].attributes.push_back( sourceLayerIndex );
499+
mFetchJoinInfo.insert( joinInfo, info );
514500
}
515501

516-
// add sourceJoinFields if we're using a subset
517-
if ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes )
518-
mRequest.setSubsetOfAttributes( mRequest.subsetOfAttributes() + sourceJoinFields );
502+
// store field source index - we'll need it when fetching from provider
503+
mFetchJoinInfo[ joinInfo ].attributes.push_back( sourceLayerIndex );
519504
}
520505

521-
void QgsVectorLayerFeatureIterator::prepareExpressions()
506+
void QgsVectorLayerFeatureIterator::prepareExpression( int fieldIdx )
522507
{
523-
const QList<QgsExpressionFieldBuffer::ExpressionField> exps = mSource->mExpressionFieldBuffer->expressions();
508+
const QList<QgsExpressionFieldBuffer::ExpressionField>& exps = mSource->mExpressionFieldBuffer->expressions();
524509

525-
mExpressionContext.reset( new QgsExpressionContext() );
526-
mExpressionContext->appendScope( QgsExpressionContextUtils::globalScope() );
527-
mExpressionContext->appendScope( QgsExpressionContextUtils::projectScope() );
528-
mExpressionContext->setFields( mSource->mFields );
510+
int oi = mSource->mFields.fieldOriginIndex( fieldIdx );
511+
QgsExpression* exp = new QgsExpression( exps[oi].cachedExpression );
529512

530-
QList< int > virtualFieldsToFetch;
531-
for ( int i = 0; i < mSource->mFields.count(); i++ )
513+
QgsDistanceArea da;
514+
da.setSourceCrs( mSource->mCrsId );
515+
da.setEllipsoidalMode( true );
516+
da.setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) );
517+
exp->setGeomCalculator( da );
518+
exp->setDistanceUnits( QgsProject::instance()->distanceUnits() );
519+
exp->setAreaUnits( QgsProject::instance()->areaUnits() );
520+
521+
exp->prepare( mExpressionContext.data() );
522+
mExpressionFieldInfo.insert( fieldIdx, exp );
523+
524+
Q_FOREACH ( const QString& col, exp->referencedColumns() )
532525
{
533-
if ( mSource->mFields.fieldOrigin( i ) == QgsFields::OriginExpression )
526+
int dependantFieldIdx = mSource->mFields.fieldNameIndex( col );
527+
if ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes )
534528
{
535-
// Only prepare if there is no subset defined or the subset contains this field
536-
if ( !( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes )
537-
|| mRequest.subsetOfAttributes().contains( i ) )
538-
{
539-
virtualFieldsToFetch << i;
540-
}
529+
mRequest.setSubsetOfAttributes( mRequest.subsetOfAttributes() << dependantFieldIdx );
541530
}
531+
// also need to fetch this dependant field
532+
if ( !mPreparedFields.contains( dependantFieldIdx ) && !mFieldsToPrepare.contains( dependantFieldIdx ) )
533+
mFieldsToPrepare << dependantFieldIdx;
542534
}
543535

544-
QList< int > virtualFieldsProcessed;
545-
while ( !virtualFieldsToFetch.isEmpty() )
536+
if ( exp->needsGeometry() )
546537
{
547-
int fieldIdx = virtualFieldsToFetch.takeFirst();
548-
if ( virtualFieldsProcessed.contains( fieldIdx ) )
549-
continue;
538+
mRequest.setFlags( mRequest.flags() & ~QgsFeatureRequest::NoGeometry );
539+
}
540+
}
550541

551-
virtualFieldsProcessed << fieldIdx;
542+
void QgsVectorLayerFeatureIterator::prepareFields()
543+
{
544+
mPreparedFields.clear();
545+
mFieldsToPrepare.clear();
546+
mFetchJoinInfo.clear();
552547

548+
mExpressionContext.reset( new QgsExpressionContext() );
549+
mExpressionContext->appendScope( QgsExpressionContextUtils::globalScope() );
550+
mExpressionContext->appendScope( QgsExpressionContextUtils::projectScope() );
551+
mExpressionContext->setFields( mSource->mFields );
553552

554-
int oi = mSource->mFields.fieldOriginIndex( fieldIdx );
555-
QgsExpression* exp = new QgsExpression( exps[oi].cachedExpression );
553+
mFieldsToPrepare = ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes ) ? mRequest.subsetOfAttributes() : mSource->mFields.allAttributesList();
556554

557-
QgsDistanceArea da;
558-
da.setSourceCrs( mSource->mCrsId );
559-
da.setEllipsoidalMode( true );
560-
da.setEllipsoid( QgsProject::instance()->readEntry( "Measure", "/Ellipsoid", GEO_NONE ) );
561-
exp->setGeomCalculator( da );
562-
exp->setDistanceUnits( QgsProject::instance()->distanceUnits() );
563-
exp->setAreaUnits( QgsProject::instance()->areaUnits() );
555+
while ( !mFieldsToPrepare.isEmpty() )
556+
{
557+
int fieldIdx = mFieldsToPrepare.takeFirst();
558+
if ( mPreparedFields.contains( fieldIdx ) )
559+
continue;
564560

565-
exp->prepare( mExpressionContext.data() );
566-
mExpressionFieldInfo.insert( fieldIdx, exp );
561+
mPreparedFields << fieldIdx;
562+
prepareField( fieldIdx );
563+
}
564+
}
567565

568-
Q_FOREACH ( const QString& col, exp->referencedColumns() )
569-
{
570-
int dependantFieldIdx = mSource->mFields.fieldNameIndex( col );
571-
if ( mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes )
566+
void QgsVectorLayerFeatureIterator::prepareField( int fieldIdx )
567+
{
568+
switch ( mSource->mFields.fieldOrigin( fieldIdx ) )
569+
{
570+
case QgsFields::OriginExpression:
571+
prepareExpression( fieldIdx );
572+
break;
573+
574+
case QgsFields::OriginJoin:
575+
if ( mSource->mJoinBuffer->containsJoins() )
572576
{
573-
mRequest.setSubsetOfAttributes( mRequest.subsetOfAttributes() << dependantFieldIdx );
577+
prepareJoin( fieldIdx );
574578
}
575-
// also need to fetch this dependant field
576-
if ( mSource->mFields.fieldOrigin( dependantFieldIdx ) == QgsFields::OriginExpression )
577-
virtualFieldsToFetch << dependantFieldIdx;
578-
}
579+
break;
579580

580-
if ( exp->needsGeometry() )
581-
{
582-
mRequest.setFlags( mRequest.flags() & ~QgsFeatureRequest::NoGeometry );
583-
}
581+
case QgsFields::OriginUnknown:
582+
case QgsFields::OriginProvider:
583+
case QgsFields::OriginEdit:
584+
break;
584585
}
585-
586586
}
587587

588588
void QgsVectorLayerFeatureIterator::addJoinedAttributes( QgsFeature &f )
@@ -612,24 +612,48 @@ void QgsVectorLayerFeatureIterator::addVirtualAttributes( QgsFeature& f )
612612
attr.resize( mSource->mFields.count() ); // Provider attrs count + joined attrs count + expression attrs count
613613
f.setAttributes( attr );
614614

615+
// possible TODO - handle combinations of expression -> join -> expression -> join?
616+
// but for now, write that off as too complex and an unlikely rare, unsupported use case
617+
618+
QList< int > fetchedVirtualAttributes;
619+
//first, check through joins for any virtual fields we need
620+
QMap<const QgsVectorJoinInfo*, FetchJoinInfo>::const_iterator joinIt = mFetchJoinInfo.constBegin();
621+
for ( ; joinIt != mFetchJoinInfo.constEnd(); ++joinIt )
622+
{
623+
if ( mExpressionFieldInfo.contains( joinIt->targetField ) )
624+
{
625+
// have to calculate expression field before we can handle this join
626+
addExpressionAttribute( f, joinIt->targetField );
627+
fetchedVirtualAttributes << joinIt->targetField;
628+
}
629+
}
630+
615631
if ( !mFetchJoinInfo.isEmpty() )
616632
addJoinedAttributes( f );
617633

634+
// add remaining expression fields
618635
if ( !mExpressionFieldInfo.isEmpty() )
619636
{
620637
QMap<int, QgsExpression*>::ConstIterator it = mExpressionFieldInfo.constBegin();
621-
622638
for ( ; it != mExpressionFieldInfo.constEnd(); ++it )
623639
{
624-
QgsExpression* exp = it.value();
625-
mExpressionContext->setFeature( f );
626-
QVariant val = exp->evaluate( mExpressionContext.data() );
627-
mSource->mFields.at( it.key() ).convertCompatible( val );
628-
f.setAttribute( it.key(), val );
640+
if ( fetchedVirtualAttributes.contains( it.key() ) )
641+
continue;
642+
643+
addExpressionAttribute( f, it.key() );
629644
}
630645
}
631646
}
632647

648+
void QgsVectorLayerFeatureIterator::addExpressionAttribute( QgsFeature& f, int attrIndex )
649+
{
650+
QgsExpression* exp = mExpressionFieldInfo.value( attrIndex );
651+
mExpressionContext->setFeature( f );
652+
QVariant val = exp->evaluate( mExpressionContext.data() );
653+
mSource->mFields.at( attrIndex ).convertCompatible( val );
654+
f.setAttribute( attrIndex, val );
655+
}
656+
633657
bool QgsVectorLayerFeatureIterator::prepareSimplification( const QgsSimplifyMethod& simplifyMethod )
634658
{
635659
Q_UNUSED( simplifyMethod );

‎src/core/qgsvectorlayerfeatureiterator.h

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,10 +97,19 @@ class CORE_EXPORT QgsVectorLayerFeatureIterator : public QgsAbstractFeatureItera
9797

9898
//! @note not available in Python bindings
9999
void rewindEditBuffer();
100+
101+
//! @note not available in Python bindings
102+
void prepareJoin( int fieldIdx );
103+
100104
//! @note not available in Python bindings
101-
void prepareJoins();
105+
void prepareExpression( int fieldIdx );
106+
102107
//! @note not available in Python bindings
103-
void prepareExpressions();
108+
void prepareFields();
109+
110+
//! @note not available in Python bindings
111+
void prepareField( int fieldIdx );
112+
104113
//! @note not available in Python bindings
105114
bool fetchNextAddedFeature( QgsFeature& f );
106115
//! @note not available in Python bindings
@@ -127,6 +136,14 @@ class CORE_EXPORT QgsVectorLayerFeatureIterator : public QgsAbstractFeatureItera
127136
*/
128137
void addVirtualAttributes( QgsFeature &f );
129138

139+
/** Adds an expression based attribute to a feature
140+
* @param f feature
141+
* @param attrIndex attribute index
142+
* @note added in QGIS 2.14
143+
* @note not available in Python bindings
144+
*/
145+
void addExpressionAttribute( QgsFeature& f, int attrIndex );
146+
130147
/** Update feature with uncommited attribute updates.
131148
* @note not available in Python bindings
132149
*/
@@ -178,6 +195,9 @@ class CORE_EXPORT QgsVectorLayerFeatureIterator : public QgsAbstractFeatureItera
178195

179196
QgsInterruptionChecker* mInterruptionChecker;
180197

198+
QList< int > mPreparedFields;
199+
QList< int > mFieldsToPrepare;
200+
181201
/**
182202
* Will always return true. We assume that ordering has been done on provider level already.
183203
*

‎tests/src/python/test_qgsfeatureiterator.py

Lines changed: 86 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import os
1818

19-
from qgis.core import QgsVectorLayer, QgsFeatureRequest, QgsFeature, QgsField, NULL
19+
from qgis.core import QgsVectorLayer, QgsFeatureRequest, QgsFeature, QgsField, NULL, QgsMapLayerRegistry, QgsVectorJoinInfo
2020
from qgis.testing import start_app, unittest
2121
from qgis.PyQt.QtCore import QVariant
2222

@@ -156,6 +156,91 @@ def test_ExpressionFieldNestedCircular(self):
156156
fet = next(layer.getFeatures(QgsFeatureRequest().setSubsetOfAttributes(['exp2'], layer.fields())))
157157
self.assertEqual(fet['Class'], NULL)
158158

159+
def test_JoinUsingExpression(self):
160+
""" test joining a layer using a virtual field """
161+
joinLayer = QgsVectorLayer(
162+
"Point?field=x:string&field=y:integer&field=z:integer",
163+
"joinlayer", "memory")
164+
pr = joinLayer.dataProvider()
165+
f1 = QgsFeature()
166+
f1.setAttributes(["foo", 246, 321])
167+
f2 = QgsFeature()
168+
f2.setAttributes(["bar", 456, 654])
169+
self.assertTrue(pr.addFeatures([f1, f2]))
170+
171+
layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
172+
"addfeat", "memory")
173+
pr = layer.dataProvider()
174+
f = QgsFeature()
175+
f.setAttributes(["test", 123])
176+
self.assertTrue(pr.addFeatures([f]))
177+
layer.addExpressionField('"fldint"*2', QgsField('exp1', QVariant.LongLong))
178+
179+
QgsMapLayerRegistry.instance().addMapLayers([layer, joinLayer])
180+
181+
join = QgsVectorJoinInfo()
182+
join.targetFieldName = "exp1"
183+
join.joinLayerId = joinLayer.id()
184+
join.joinFieldName = "y"
185+
join.memoryCache = True
186+
layer.addJoin(join)
187+
188+
f = QgsFeature()
189+
fi = layer.getFeatures()
190+
self.assertTrue(fi.nextFeature(f))
191+
attrs = f.attributes()
192+
self.assertEqual(attrs[0], "test")
193+
self.assertEqual(attrs[1], 123)
194+
self.assertEqual(attrs[2], "foo")
195+
self.assertEqual(attrs[3], 321)
196+
self.assertEqual(attrs[4], 246)
197+
self.assertFalse(fi.nextFeature(f))
198+
199+
QgsMapLayerRegistry.instance().removeMapLayers([layer, joinLayer])
200+
201+
def test_JoinUsingExpression2(self):
202+
""" test joining a layer using a virtual field (the other way!) """
203+
joinLayer = QgsVectorLayer(
204+
"Point?field=x:string&field=y:integer&field=z:integer",
205+
"joinlayer", "memory")
206+
pr = joinLayer.dataProvider()
207+
f1 = QgsFeature()
208+
f1.setAttributes(["foo", 246, 321])
209+
f2 = QgsFeature()
210+
f2.setAttributes(["bar", 456, 654])
211+
self.assertTrue(pr.addFeatures([f1, f2]))
212+
joinLayer.addExpressionField('"y"/2', QgsField('exp1', QVariant.LongLong))
213+
214+
layer = QgsVectorLayer("Point?field=fldtxt:string&field=fldint:integer",
215+
"addfeat", "memory")
216+
pr = layer.dataProvider()
217+
f = QgsFeature()
218+
f.setAttributes(["test", 123])
219+
self.assertTrue(pr.addFeatures([f]))
220+
221+
QgsMapLayerRegistry.instance().addMapLayers([layer, joinLayer])
222+
223+
join = QgsVectorJoinInfo()
224+
join.targetFieldName = "fldint"
225+
join.joinLayerId = joinLayer.id()
226+
join.joinFieldName = "exp1"
227+
join.memoryCache = True
228+
layer.addJoin(join)
229+
230+
f = QgsFeature()
231+
fi = layer.getFeatures()
232+
self.assertTrue(fi.nextFeature(f))
233+
attrs = f.attributes()
234+
self.assertEqual(attrs[0], "test")
235+
self.assertEqual(attrs[1], 123)
236+
self.assertEqual(attrs[2], "foo")
237+
self.assertEqual(attrs[3], 246)
238+
self.assertEqual(attrs[4], 321)
239+
self.assertFalse(fi.nextFeature(f))
240+
241+
QgsMapLayerRegistry.instance().removeMapLayers([layer, joinLayer])
242+
# try the other way too
243+
159244

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

0 commit comments

Comments
 (0)
Please sign in to comment.