Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Collapse OR nodes into IN #59440

Merged
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/core/expression/qgsexpressionnode.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -97,4 +97,3 @@ void QgsExpressionNode::cloneTo( QgsExpressionNode *target ) const
target->parserFirstColumn = parserFirstColumn;
target->parserFirstLine = parserFirstLine;
}

116 changes: 116 additions & 0 deletions src/core/expression/qgsexpressionnodeimpl.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -694,6 +694,122 @@ QgsExpressionNode::NodeType QgsExpressionNodeBinaryOperator::nodeType() const

bool QgsExpressionNodeBinaryOperator::prepareNode( QgsExpression *parent, const QgsExpressionContext *context )
{

// if this is an OR, try to collapse the OR expression into an IN node
if ( mOp == boOr )
{

// First step: flatten OR chain and collect values
QMap<QString, QgsExpressionNode::NodeList> orValuesMap;
QList<QString> orFieldNames;

// Get a list of all the OR and IN nodes chained together
std::function<bool ( QgsExpressionNode * )> visitOrNodes = [&visitOrNodes, &orValuesMap, &orFieldNames]( QgsExpressionNode * node ) -> bool
{
if ( QgsExpressionNodeBinaryOperator *op = dynamic_cast<QgsExpressionNodeBinaryOperator *>( node ) )
{
if ( op->op() != boOr && op->op() != boEQ )
{
return false;
}

if ( op->op() == boEQ )
{
// If left is a column ref and right is a literal, collect
if ( dynamic_cast<QgsExpressionNodeColumnRef *>( op->opLeft() ) && dynamic_cast<QgsExpressionNodeLiteral *>( op->opRight() ) )
elpaso marked this conversation as resolved.
Show resolved Hide resolved
{
const QString fieldName = op->opLeft()->dump();
if ( !orValuesMap.contains( fieldName ) )
troopa81 marked this conversation as resolved.
Show resolved Hide resolved
{
orFieldNames.append( fieldName );
orValuesMap.insert( fieldName, QgsExpressionNode::NodeList() );
}
orValuesMap[fieldName].append( op->opRight()->clone() );
return true;
}
return false;
}

if ( visitOrNodes( op->opLeft() ) && visitOrNodes( op->opRight() ) )
{
return true;
}

}
else if ( QgsExpressionNodeInOperator *inOp = dynamic_cast<QgsExpressionNodeInOperator *>( node ) )
{
if ( inOp->node()->nodeType() != QgsExpressionNode::ntColumnRef )
{
return false;
}

const QString fieldName = inOp->node()->dump();

// Check if all nodes are literals
const auto nodes = inOp->list()->list();
for ( const auto &valueNode : std::as_const( nodes ) )
{
if ( valueNode->nodeType() != QgsExpressionNode::ntLiteral )
{
return false;
}
}

if ( !orValuesMap.contains( fieldName ) )
troopa81 marked this conversation as resolved.
Show resolved Hide resolved
{
orFieldNames.append( fieldName );
troopa81 marked this conversation as resolved.
Show resolved Hide resolved
orValuesMap.insert( fieldName, *inOp->list()->clone() );
}
else
{
for ( const auto &valueNode : std::as_const( nodes ) )
{
orValuesMap[fieldName].append( valueNode->clone() );
}
troopa81 marked this conversation as resolved.
Show resolved Hide resolved
}

return true;
}
return false;
};


// Second step: build the OR chain of IN operators
if ( visitOrNodes( this ) && ! orValuesMap.empty() )
{

// Recursively build the OR chain of IN operators
std::function<QgsExpressionNode* ( QList<QString>::const_iterator )> buildOrChain = [&buildOrChain, &orValuesMap, &orFieldNames]( QList<QString>::const_iterator fieldNameit ) -> QgsExpressionNode *
{

QgsExpressionNode *currentNode;
auto orValuesIt = orValuesMap.find( *fieldNameit );

if ( orValuesIt.value().count() == 1 )
{
currentNode = new QgsExpressionNodeBinaryOperator( boEQ, new QgsExpressionNodeColumnRef( orValuesIt.key() ), orValuesIt.value().at( 0 )->clone() );
}
else
{
currentNode = new QgsExpressionNodeInOperator( new QgsExpressionNodeColumnRef( orValuesIt.key() ), orValuesIt.value().clone() );
}

if ( fieldNameit + 1 == orFieldNames.cend() )
{
return currentNode;
}
else
{
QgsExpressionNode *nextNode = buildOrChain( fieldNameit + 1 );
return new QgsExpressionNodeBinaryOperator( boOr, currentNode, nextNode );
}
};
elpaso marked this conversation as resolved.
Show resolved Hide resolved

mCompiledSimplifiedNode.reset( buildOrChain( orFieldNames.cbegin() ) );
}

}

bool resL = mOpLeft->prepare( parent, context );
bool resR = mOpRight->prepare( parent, context );
return resL && resR;
Expand Down
17 changes: 14 additions & 3 deletions src/gui/qgsrelationeditorwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -626,8 +626,10 @@ void QgsRelationEditorWidget::updateUiSingleEdit()
QgsVectorLayer *layer = nullptr;
if ( mNmRelation.isValid() )
{
QgsFeatureIterator it = mRelation.referencingLayer()->getFeatures( request );
QgsFeature fet;
QgsFeatureRequest nmRequest;

QgsFeatureIterator it = mRelation.referencingLayer()->getFeatures( request );
QStringList filters;

while ( it.nextFeature( fet ) )
Expand All @@ -636,8 +638,15 @@ void QgsRelationEditorWidget::updateUiSingleEdit()
filters << filter.prepend( '(' ).append( ')' );
}

QgsFeatureRequest nmRequest;
nmRequest.setFilterExpression( filters.join( QLatin1String( " OR " ) ) );
QString reducedExpression;
if ( QgsExpression::attemptReduceToInClause( filters, reducedExpression ) )
troopa81 marked this conversation as resolved.
Show resolved Hide resolved
{
nmRequest.setFilterExpression( reducedExpression );
}
else
{
nmRequest.setFilterExpression( filters.join( QStringLiteral( " OR " ) ) );
}

request = nmRequest;
layer = mNmRelation.referencedLayer();
Expand Down Expand Up @@ -666,6 +675,8 @@ void QgsRelationEditorWidget::updateUiSingleEdit()

updateButtons();
}

mFeatureRequest = request;
}

void QgsRelationEditorWidget::updateUiMultiEdit()
Expand Down
3 changes: 3 additions & 0 deletions src/gui/qgsrelationeditorwidget.h
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@ class GUI_EXPORT QgsRelationEditorWidget : public QgsAbstractRelationEditorWidge
bool mAllowAddChildFeatureWithNoGeometry = true;
QString mFilterExpression;

// Mainly used for testing purposes
QgsFeatureRequest mFeatureRequest;
elpaso marked this conversation as resolved.
Show resolved Hide resolved

QList<QTreeWidgetItem *> mMultiEditPreviousSelectedItems;
QgsFeatureIds mMultiEdit1NJustAddedIds;

Expand Down
34 changes: 34 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -5589,6 +5589,40 @@ class TestQgsExpression: public QObject
}
}

void testCollapseOrValues_data()
{
QTest::addColumn<QString>( "expression" );
QTest::addColumn<QString>( "expected" );


QTest::newRow( "simple" ) << QStringLiteral( "field = 'value' OR field = 'value2'" ) << QStringLiteral( "field IN ('value', 'value2')" );
QTest::newRow( "simple 2" ) << QStringLiteral( "\"field\" = 'value' OR field = 'value2' OR field = 'value3'" ) << QStringLiteral( "field IN ('value', 'value2', 'value3')" );
QTest::newRow( "simple3 mixed" ) << QStringLiteral( "field = 'value' OR field = 'value2' OR field2 = 'value3'" ) << QStringLiteral( "field IN ('value', 'value2') OR field2 = 'value3'" );
QTest::newRow( "simple3 mixed 2" ) << QStringLiteral( "field2 = 'value3' OR field = 'value1' OR field = 'value2'" ) << QStringLiteral( "field2 = 'value3' OR field IN ('value1', 'value2')" );
QTest::newRow( "simple3 mixed 3" ) << QStringLiteral( "field = 'value1' OR field2 = 'value3' OR field = 'value2'" ) << QStringLiteral( "field IN ('value1', 'value2') OR field2 = 'value3'" );

// test with IN
QTest::newRow( "simple IN" ) << QStringLiteral( "field IN ('value', 'value2') OR field = 'value3'" ) << QStringLiteral( "field IN ('value', 'value2', 'value3')" );
QTest::newRow( "simple IN 2" ) << QStringLiteral( "field = 'value' OR field IN ('value2', 'value3')" ) << QStringLiteral( "field IN ('value', 'value2', 'value3')" );

// test cases that should not trigger reduction
QTest::newRow( "no reduction 1" ) << QStringLiteral( "field = 'value' OR field2 = 'value2'" ) << QStringLiteral( "field = 'value' OR field2 = 'value2'" );
// this could theoretically be reduced, but we don't currently support this
QTest::newRow( "no reduction 2" ) << QStringLiteral( "field = 'value' OR field != 'value2' OR field = 'value3'" ) << QStringLiteral( "field = 'value' OR field <> 'value2' OR field = 'value3'" );

}

void testCollapseOrValues()
{
QFETCH( QString, expression );
QFETCH( QString, expected );

QgsExpression exp( expression );
QgsExpressionContext context;
exp.prepare( &context );
QCOMPARE( exp.rootNode()->effectiveNode()->dump(), expected );
}

void testPrecomputedNodesWithIntrospectionFunctions()
{
QgsFields fields;
Expand Down
27 changes: 26 additions & 1 deletion tests/src/gui/testqgsrelationeditorwidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ class TestQgsRelationEditorWidget : public QObject

void testMultiEdit1N();
void testMultiEditNM();
void testFeatureRequest();
void testUpdateUi();

private:
Expand Down Expand Up @@ -159,7 +160,7 @@ void TestQgsRelationEditorWidget::init()
mLayerJoin->commitChanges();

QgsFeature jft3( mLayerJoin->fields() );
jft3.setAttribute( QStringLiteral( "pk" ), 102 );
jft3.setAttribute( QStringLiteral( "pk" ), 103 );
jft3.setAttribute( QStringLiteral( "fk_layer1" ), 0 );
jft3.setAttribute( QStringLiteral( "fk_layer2" ), 11 );
mLayerJoin->startEditing();
Expand Down Expand Up @@ -285,6 +286,30 @@ void TestQgsRelationEditorWidget::testMultiEditNM()

}

void TestQgsRelationEditorWidget::testFeatureRequest()
{

// Init a relation editor widget
QgsRelationEditorWidget relationEditorWidget( QVariantMap(),
new QWidget() );
relationEditorWidget.setRelations( *mRelation1N, *mRelationNM );

QVERIFY( !relationEditorWidget.multiEditModeActive() );

QgsFeatureIterator featureIterator = mLayer1->getFeatures();
QgsFeature feature;
featureIterator.nextFeature( feature );
relationEditorWidget.setFeature( feature );

QgsAttributeEditorContext context;
QgsTrackedVectorLayerTools tools;
context.setVectorLayerTools( &tools );
relationEditorWidget.setEditorContext( context );

relationEditorWidget.updateUiSingleEdit();
QCOMPARE( relationEditorWidget.mFeatureRequest.filterExpression()->expression(), QStringLiteral( "\"pk\" IN (10,11)" ) );
}

void TestQgsRelationEditorWidget::testUpdateUi()
{
// Test that we don't recreate all the widget when only the request have been updated
Expand Down
Loading