Skip to content

Commit

Permalink
Add utility functions to determine if a QgsExpression is a simple
Browse files Browse the repository at this point in the history
"field=value" type expression, and for condensing a list of similar
expressions to an equivalent "field IN (value, value2,...)" expression
wherever possible
  • Loading branch information
nyalldawson committed Jan 12, 2021
1 parent 9b7283a commit 44c2a26
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 0 deletions.
30 changes: 30 additions & 0 deletions python/core/auto_generated/expression/qgsexpression.sip.in
Expand Up @@ -614,6 +614,36 @@ value. The value may be null.
:return: the expression to evaluate field equality

.. versionadded:: 3.0
%End

static bool isFieldEqualityExpression( const QString &expression, QString &field /Out/, QVariant &value /Out/ );
%Docstring
Returns ``True`` if the given ``expression`` is a simple "field=value" type expression.

:param expression: expression to test

:return: - ``True`` if the expression is a field equality expression
- field: will be set to the field name if the expression is a field equality expression
- value: will be set to the value if the expression is a field equality expression

.. versionadded:: 3.18
%End

static bool attemptReduceToInClause( const QStringList &expressions, QString &result /Out/ );
%Docstring
Attempts to reduce a list of expressions to a single "field IN (val1, val2, ... )" type expression.

This will only be possible if all the input expressions form simple "field=value" OR "field IN (value1, value2)" expressions, and all
reference the same field name.

Returns ``True`` if the given ``expressions`` could be converted to an IN type expression.

:param expressions: expressions to test

:return: - ``True`` if the expression was converted to a field IN type expression
- result: will be set to the calculated "field IN (...)" expression, wherever possible

.. versionadded:: 3.18
%End

SIP_PYOBJECT __repr__();
Expand Down
96 changes: 96 additions & 0 deletions src/core/expression/qgsexpression.cpp
Expand Up @@ -1089,6 +1089,102 @@ QString QgsExpression::createFieldEqualityExpression( const QString &fieldName,
return expr;
}

bool QgsExpression::isFieldEqualityExpression( const QString &expression, QString &field, QVariant &value )
{
QgsExpression e( expression );

if ( !e.rootNode() )
return false;

if ( const QgsExpressionNodeBinaryOperator *binOp = dynamic_cast<const QgsExpressionNodeBinaryOperator *>( e.rootNode() ) )
{
if ( binOp->op() == QgsExpressionNodeBinaryOperator::boEQ )
{
const QgsExpressionNodeColumnRef *columnRef = dynamic_cast<const QgsExpressionNodeColumnRef *>( binOp->opLeft() );
const QgsExpressionNodeLiteral *literal = dynamic_cast<const QgsExpressionNodeLiteral *>( binOp->opRight() );
if ( columnRef && literal )
{
field = columnRef->name();
value = literal->value();
return true;
}
}
}
return false;
}

bool QgsExpression::attemptReduceToInClause( const QStringList &expressions, QString &result )
{
if ( expressions.empty() )
return false;

QString inField;
bool first = true;
QStringList values;
for ( const QString &expression : expressions )
{
QString field;
QVariant value;
if ( QgsExpression::isFieldEqualityExpression( expression, field, value ) )
{
if ( first )
{
inField = field;
first = false;
}
else if ( field != inField )
{
return false;
}
values << QgsExpression::quotedValue( value );
}
else
{
// we also allow reducing similar 'field IN (...)' expressions!
QgsExpression e( expression );

if ( !e.rootNode() )
return false;

if ( const QgsExpressionNodeInOperator *inOp = dynamic_cast<const QgsExpressionNodeInOperator *>( e.rootNode() ) )
{
const QgsExpressionNodeColumnRef *columnRef = dynamic_cast<const QgsExpressionNodeColumnRef *>( inOp->node() );
if ( !columnRef )
return false;

if ( first )
{
inField = columnRef->name();
first = false;
}
else if ( columnRef->name() != inField )
{
return false;
}

if ( QgsExpressionNode::NodeList *nodeList = inOp->list() )
{
const QList<QgsExpressionNode *> nodes = nodeList->list();
for ( const QgsExpressionNode *node : nodes )
{
const QgsExpressionNodeLiteral *literal = dynamic_cast<const QgsExpressionNodeLiteral *>( node );
if ( !literal )
return false;

values << QgsExpression::quotedValue( literal->value() );
}
}
}
else
{
return false;
}
}
}
result = QStringLiteral( "%1 IN (%2)" ).arg( inField, values.join( ',' ) );
return true;
}

const QgsExpressionNode *QgsExpression::rootNode() const
{
return d->mRootNode;
Expand Down
28 changes: 28 additions & 0 deletions src/core/expression/qgsexpression.h
Expand Up @@ -610,6 +610,34 @@ class CORE_EXPORT QgsExpression
*/
static QString createFieldEqualityExpression( const QString &fieldName, const QVariant &value );

/**
* Returns TRUE if the given \a expression is a simple "field=value" type expression.
*
* \param expression expression to test
* \param field will be set to the field name if the expression is a field equality expression
* \param value will be set to the value if the expression is a field equality expression
* \returns TRUE if the expression is a field equality expression
*
* \since QGIS 3.18
*/
static bool isFieldEqualityExpression( const QString &expression, QString &field SIP_OUT, QVariant &value SIP_OUT );

/**
* Attempts to reduce a list of expressions to a single "field IN (val1, val2, ... )" type expression.
*
* This will only be possible if all the input expressions form simple "field=value" OR "field IN (value1, value2)" expressions, and all
* reference the same field name.
*
* Returns TRUE if the given \a expressions could be converted to an IN type expression.
*
* \param expressions expressions to test
* \param result will be set to the calculated "field IN (...)" expression, wherever possible
* \returns TRUE if the expression was converted to a field IN type expression
*
* \since QGIS 3.18
*/
static bool attemptReduceToInClause( const QStringList &expressions, QString &result SIP_OUT );

#ifdef SIP_RUN
SIP_PYOBJECT __repr__();
% MethodCode
Expand Down
67 changes: 67 additions & 0 deletions tests/src/core/testqgsexpression.cpp
Expand Up @@ -4049,6 +4049,73 @@ class TestQgsExpression: public QObject
QCOMPARE( res.toString(), QStringLiteral( "test" ) );
}

void testIsFieldEqualityExpression_data()
{
QTest::addColumn<QString>( "input" );
QTest::addColumn<bool>( "expected" );
QTest::addColumn<QString>( "field" );
QTest::addColumn<QVariant>( "value" );
QTest::newRow( "empty" ) << "" << false << QString() << QVariant();
QTest::newRow( "invalid" ) << "a=" << false << QString() << QVariant();
QTest::newRow( "is string" ) << "field = 'value'" << true << "field" << QVariant( QStringLiteral( "value" ) );
QTest::newRow( "is number" ) << "field = 5" << true << "field" << QVariant( 5 );
QTest::newRow( "quoted field" ) << "\"my field\" = 5" << true << "my field" << QVariant( 5 );
QTest::newRow( "not equal" ) << "field <> 5" << false << QString() << QVariant();
}

void testIsFieldEqualityExpression()
{
QFETCH( QString, input );
QFETCH( bool, expected );
QFETCH( QString, field );
QFETCH( QVariant, value );

QString resField;
QVariant resValue;
QCOMPARE( QgsExpression::isFieldEqualityExpression( input, resField, resValue ), expected );
if ( expected )
{
QCOMPARE( resField, field );
QCOMPARE( resValue, value );
}
}

void testAttemptReduceToInClause_data()
{
QTest::addColumn<QStringList>( "input" );
QTest::addColumn<bool>( "expected" );
QTest::addColumn<QString>( "expression" );
QTest::newRow( "empty" ) << QStringList() << false << QString();
QTest::newRow( "invalid" ) << ( QStringList() << QStringLiteral( "a=" ) ) << false << QString();
QTest::newRow( "not equality" ) << ( QStringList() << QStringLiteral( "field <> 'value'" ) ) << false << "field";
QTest::newRow( "one expression" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) ) << true << "field IN ('value')";
QTest::newRow( "one IN expression" ) << ( QStringList() << QStringLiteral( "field IN ('value', 'value2')" ) ) << true << "field IN ('value','value2')";
QTest::newRow( "one IN expression non-literal" ) << ( QStringList() << QStringLiteral( "field IN ('value', 'value2', \"a field\")" ) ) << false << QString();
QTest::newRow( "two expressions" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field = 'value2'" ) ) << true << "field IN ('value','value2')";
QTest::newRow( "two expressions with IN" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field IN ('value2', 'value3')" ) ) << true << "field IN ('value','value2','value3')";
QTest::newRow( "two expressions with IN not literal" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field IN ('value2', 'value3', \"a field\")" ) ) << false << QString();
QTest::newRow( "two expressions with IN different field" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field2 IN ('value2', 'value3')" ) ) << false << QString();
QTest::newRow( "two expressions first not equality" ) << ( QStringList() << QStringLiteral( "field <>'value'" ) << QStringLiteral( "field == 'value2'" ) ) << false << "field";
QTest::newRow( "two expressions second not equality" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field <> 'value2'" ) ) << false << "field";
QTest::newRow( "three expressions" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field = 'value2'" ) << QStringLiteral( "field = 'value3'" ) ) << true << "field IN ('value','value2','value3')";
QTest::newRow( "three expressions with IN" ) << ( QStringList() << QStringLiteral( "field IN ('v1', 'v2')" ) << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field = 'value2'" ) << QStringLiteral( "field = 'value3'" ) ) << true << "field IN ('v1','v2','value','value2','value3')";
QTest::newRow( "three expressions different fields" ) << ( QStringList() << QStringLiteral( "field = 'value'" ) << QStringLiteral( "field2 = 'value2'" ) << QStringLiteral( "field = 'value3'" ) ) << false << "field";
}

void testAttemptReduceToInClause()
{
QFETCH( QStringList, input );
QFETCH( bool, expected );
QFETCH( QString, expression );

QString resExpression;
QCOMPARE( QgsExpression::attemptReduceToInClause( input, resExpression ), expected );
if ( expected )
{
QCOMPARE( resExpression, expression );
}
}

};

QGSTEST_MAIN( TestQgsExpression )
Expand Down

0 comments on commit 44c2a26

Please sign in to comment.