From 06d5f924f6846cf4f7540bf86af7fb2d464cf9c1 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Mon, 17 Dec 2018 11:31:04 +1000 Subject: [PATCH] More square brackets --- .../expression/qgsexpressionnode.sip.in | 3 +- .../expression/qgsexpressionnodeimpl.sip.in | 57 +++++++++++ resources/function_help/json/op_index | 15 +++ src/core/expression/qgsexpressionnode.h | 3 +- src/core/expression/qgsexpressionnodeimpl.cpp | 94 ++++++++++++++++++ src/core/expression/qgsexpressionnodeimpl.h | 51 ++++++++++ src/core/qgsexpressionparser.yy | 14 +-- src/core/qgssqlexpressioncompiler.cpp | 3 + src/gui/qgsexpressionbuilderwidget.cpp | 8 +- .../ogr/qgsogrexpressioncompiler.cpp | 1 + tests/src/core/testqgsexpression.cpp | 63 ++++++++++++ .../qgis_server_accesscontrol/geo.gpkg | Bin 77824 -> 77824 bytes 12 files changed, 299 insertions(+), 13 deletions(-) create mode 100644 resources/function_help/json/op_index diff --git a/python/core/auto_generated/expression/qgsexpressionnode.sip.in b/python/core/auto_generated/expression/qgsexpressionnode.sip.in index 2250e4301c5..d48390b3124 100644 --- a/python/core/auto_generated/expression/qgsexpressionnode.sip.in +++ b/python/core/auto_generated/expression/qgsexpressionnode.sip.in @@ -60,7 +60,8 @@ Abstract base class for all nodes that can appear in an expression. ntFunction, ntLiteral, ntColumnRef, - ntCondition + ntCondition, + ntIndexOperator, }; diff --git a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in index f0a9000b9b5..cafebd0eb0b 100644 --- a/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in +++ b/python/core/auto_generated/expression/qgsexpressionnodeimpl.sip.in @@ -179,6 +179,63 @@ Returns a the name of this operator without the operands. I.e. "AND", "OR", ... %End +}; + +class QgsExpressionNodeIndexOperator : QgsExpressionNode +{ +%Docstring +A indexing expression operator, which allows use of square brackets [] to reference map and array items. + +.. versionadded:: 3.6 +%End + +%TypeHeaderCode +#include "qgsexpressionnodeimpl.h" +%End + public: + + QgsExpressionNodeIndexOperator( QgsExpressionNode *container /Transfer/, QgsExpressionNode *index /Transfer/ ); +%Docstring +Constructor for QgsExpressionNodeIndexOperator. +%End + ~QgsExpressionNodeIndexOperator(); + + QgsExpressionNode *container() const; +%Docstring +Returns the container node, representing an array or map value. + +.. seealso:: :py:func:`index` +%End + + QgsExpressionNode *index() const; +%Docstring +Returns the index node, representing an array element index or map key. + +.. seealso:: :py:func:`container` +%End + + virtual QgsExpressionNode::NodeType nodeType() const; + + virtual bool prepareNode( QgsExpression *parent, const QgsExpressionContext *context ); + + virtual QVariant evalNode( QgsExpression *parent, const QgsExpressionContext *context ); + + virtual QString dump() const; + + + virtual QSet referencedColumns() const; + + virtual QSet referencedVariables() const; + + virtual QSet referencedFunctions() const; + + virtual bool needsGeometry() const; + + virtual QgsExpressionNode *clone() const /Factory/; + + virtual bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const; + + }; class QgsExpressionNodeInOperator : QgsExpressionNode diff --git a/resources/function_help/json/op_index b/resources/function_help/json/op_index new file mode 100644 index 00000000000..7cb978b0995 --- /dev/null +++ b/resources/function_help/json/op_index @@ -0,0 +1,15 @@ +{ + "name": "[]", + "type": "operator", + "description": "Index operator. Returns an element from an array or map value.", + "arguments": [ + { "arg": "index", "description": "array index or map key value" } + ], + "examples": [ + { "expression":"array(1,2,3)[0]", "returns":"1"}, + { "expression":"array(1,2,3)[2]", "returns":"3"}, + { "expression":"array(1,2,3)[-1]", "returns":"3"}, + { "expression":"map('a',1,'b',2)['a']", "returns":"1"}, + { "expression":"map('a',1,'b',2)['b']", "returns":"2"} + ] +} diff --git a/src/core/expression/qgsexpressionnode.h b/src/core/expression/qgsexpressionnode.h index c28c1d3193d..886cbee34ae 100644 --- a/src/core/expression/qgsexpressionnode.h +++ b/src/core/expression/qgsexpressionnode.h @@ -79,7 +79,8 @@ class CORE_EXPORT QgsExpressionNode SIP_ABSTRACT ntFunction, //!< \see QgsExpression::Node::NodeFunction ntLiteral, //!< \see QgsExpression::Node::NodeLiteral ntColumnRef, //!< \see QgsExpression::Node::NodeColumnRef - ntCondition //!< \see QgsExpression::Node::NodeCondition + ntCondition, //!< \see QgsExpression::Node::NodeCondition + ntIndexOperator, //!< Index operator }; diff --git a/src/core/expression/qgsexpressionnodeimpl.cpp b/src/core/expression/qgsexpressionnodeimpl.cpp index 895cf907f02..454e9ba2f30 100644 --- a/src/core/expression/qgsexpressionnodeimpl.cpp +++ b/src/core/expression/qgsexpressionnodeimpl.cpp @@ -1548,3 +1548,97 @@ QString QgsExpressionNodeBinaryOperator::text() const return BINARY_OPERATOR_TEXT[mOp]; } +// + +QVariant QgsExpressionNodeIndexOperator::evalNode( QgsExpression *parent, const QgsExpressionContext *context ) +{ + const QVariant container = mContainer->eval( parent, context ); + ENSURE_NO_EVAL_ERROR; + const QVariant index = mIndex->eval( parent, context ); + ENSURE_NO_EVAL_ERROR; + + switch ( container.type() ) + { + case QVariant::Map: + return QgsExpressionUtils::getMapValue( container, parent ).value( index.toString() ); + + case QVariant::List: + case QVariant::StringList: + { + const QVariantList list = QgsExpressionUtils::getListValue( container, parent ); + qlonglong pos = QgsExpressionUtils::getIntValue( index, parent ); + if ( pos >= list.length() || pos < -list.length() ) + { + return QVariant(); + } + if ( pos < 0 ) + { + // negative indices are from back of list + pos += list.length(); + } + + return list.at( pos ); + } + + default: + parent->setEvalErrorString( tr( "[] can only be used with map or array values, not %1" ).arg( QMetaType::typeName( container.type() ) ) ); + return QVariant(); + } +} + +QgsExpressionNode::NodeType QgsExpressionNodeIndexOperator::nodeType() const +{ + return ntIndexOperator; +} + +bool QgsExpressionNodeIndexOperator::prepareNode( QgsExpression *parent, const QgsExpressionContext *context ) +{ + bool resC = mContainer->prepare( parent, context ); + bool resV = mIndex->prepare( parent, context ); + return resC && resV; +} + +QString QgsExpressionNodeIndexOperator::dump() const +{ + return QStringLiteral( "%1[%2]" ).arg( mContainer->dump(), mIndex->dump() ); +} + +QSet QgsExpressionNodeIndexOperator::referencedColumns() const +{ + return mContainer->referencedColumns() + mIndex->referencedColumns(); +} + +QSet QgsExpressionNodeIndexOperator::referencedVariables() const +{ + return mContainer->referencedVariables() + mIndex->referencedVariables(); +} + +QSet QgsExpressionNodeIndexOperator::referencedFunctions() const +{ + return mContainer->referencedFunctions() + mIndex->referencedFunctions(); +} + +QList QgsExpressionNodeIndexOperator::nodes() const +{ + QList lst; + lst << this; + lst += mContainer->nodes() + mIndex->nodes(); + return lst; +} + +bool QgsExpressionNodeIndexOperator::needsGeometry() const +{ + return mContainer->needsGeometry() || mIndex->needsGeometry(); +} + +QgsExpressionNode *QgsExpressionNodeIndexOperator::clone() const +{ + QgsExpressionNodeIndexOperator *copy = new QgsExpressionNodeIndexOperator( mContainer->clone(), mIndex->clone() ); + cloneTo( copy ); + return copy; +} + +bool QgsExpressionNodeIndexOperator::isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const +{ + return mContainer->isStatic( parent, context ) && mIndex->isStatic( parent, context ); +} diff --git a/src/core/expression/qgsexpressionnodeimpl.h b/src/core/expression/qgsexpressionnodeimpl.h index 52f08081cb6..374cad5e1d4 100644 --- a/src/core/expression/qgsexpressionnodeimpl.h +++ b/src/core/expression/qgsexpressionnodeimpl.h @@ -206,6 +206,57 @@ class CORE_EXPORT QgsExpressionNodeBinaryOperator : public QgsExpressionNode static const char *BINARY_OPERATOR_TEXT[]; }; +/** + * A indexing expression operator, which allows use of square brackets [] to reference map and array items. + * \ingroup core + * \since QGIS 3.6 + */ +class CORE_EXPORT QgsExpressionNodeIndexOperator : public QgsExpressionNode +{ + public: + + /** + * Constructor for QgsExpressionNodeIndexOperator. + */ + QgsExpressionNodeIndexOperator( QgsExpressionNode *container SIP_TRANSFER, QgsExpressionNode *index SIP_TRANSFER ) + : mContainer( container ) + , mIndex( index ) + {} + ~QgsExpressionNodeIndexOperator() override { delete mContainer; delete mIndex; } + + /** + * Returns the container node, representing an array or map value. + * \see index() + */ + QgsExpressionNode *container() const { return mContainer; } + + /** + * Returns the index node, representing an array element index or map key. + * \see container() + */ + QgsExpressionNode *index() const { return mIndex; } + + QgsExpressionNode::NodeType nodeType() const override; + bool prepareNode( QgsExpression *parent, const QgsExpressionContext *context ) override; + QVariant evalNode( QgsExpression *parent, const QgsExpressionContext *context ) override; + QString dump() const override; + + QSet referencedColumns() const override; + QSet referencedVariables() const override; + QSet referencedFunctions() const override; + QList nodes( ) const override; SIP_SKIP + + bool needsGeometry() const override; + QgsExpressionNode *clone() const override SIP_FACTORY; + bool isStatic( QgsExpression *parent, const QgsExpressionContext *context ) const override; + + private: + + QgsExpressionNode *mContainer = nullptr; + QgsExpressionNode *mIndex = nullptr; + +}; + /** * An expression node for value IN or NOT IN clauses. * \ingroup core diff --git a/src/core/qgsexpressionparser.yy b/src/core/qgsexpressionparser.yy index 59b8c3d240d..84a38678c9b 100644 --- a/src/core/qgsexpressionparser.yy +++ b/src/core/qgsexpressionparser.yy @@ -115,7 +115,7 @@ void addParserLocation(YYLTYPE* yyloc, QgsExpressionNode *node) // // operator tokens -%token OR AND EQ NE LE GE LT GT REGEXP LIKE IS PLUS MINUS MUL DIV INTDIV MOD CONCAT POW SQUARE_BRAKET_OPENING SQUARE_BRAKET_CLOSING +%token OR AND EQ NE LE GE LT GT REGEXP LIKE IS PLUS MINUS MUL DIV INTDIV MOD CONCAT POW %token NOT %token IN @@ -166,6 +166,7 @@ void addParserLocation(YYLTYPE* yyloc, QgsExpressionNode *node) %right UMINUS // fictitious symbol (for unary minus) %left COMMA +%left '[' %destructor { delete $$; } %destructor { delete $$; } @@ -284,15 +285,8 @@ expression: | expression IN '(' exp_list ')' { $$ = new QgsExpressionNodeInOperator($1, $4, false); } | expression NOT IN '(' exp_list ')' { $$ = new QgsExpressionNodeInOperator($1, $5, true); } - | expression '[' expression ']' - { - QgsExpressionNode::NodeList* args = new QgsExpressionNode::NodeList(); - args->append( $1 ); - args->append( $3 ); - QgsExpressionNodeFunction *f = new QgsExpressionNodeFunction( QgsExpression::functionIndex( "map_get" ), args ); - $$ = f; - } - + | expression '[' expression ']' { $$ = new QgsExpressionNodeIndexOperator( $1, $3 ); } + | PLUS expression %prec UMINUS { $$ = $2; } | MINUS expression %prec UMINUS { $$ = new QgsExpressionNodeUnaryOperator( QgsExpressionNodeUnaryOperator::uoMinus, $2); } diff --git a/src/core/qgssqlexpressioncompiler.cpp b/src/core/qgssqlexpressioncompiler.cpp index bc151113a6c..39854eb7960 100644 --- a/src/core/qgssqlexpressioncompiler.cpp +++ b/src/core/qgssqlexpressioncompiler.cpp @@ -401,6 +401,9 @@ QgsSqlExpressionCompiler::Result QgsSqlExpressionCompiler::compileNode( const Qg case QgsExpressionNode::ntCondition: break; + + case QgsExpressionNode::ntIndexOperator: + break; } return Fail; diff --git a/src/gui/qgsexpressionbuilderwidget.cpp b/src/gui/qgsexpressionbuilderwidget.cpp index 164905a1550..d085ac33011 100644 --- a/src/gui/qgsexpressionbuilderwidget.cpp +++ b/src/gui/qgsexpressionbuilderwidget.cpp @@ -97,7 +97,8 @@ QgsExpressionBuilderWidget::QgsExpressionBuilderWidget( QWidget *parent ) connect( mShowHelpButton, &QPushButton::clicked, this, [ = ]() { functionsplit->setSizes( QList( {mOperationListGroup->width() - mHelpAndValuesWidget->minimumWidth(), - mHelpAndValuesWidget->minimumWidth()} ) ); + mHelpAndValuesWidget->minimumWidth() + } ) ); mShowHelpButton->setEnabled( false ); } ); connect( functionsplit, &QSplitter::splitterMoved, this, [ = ]( int, int ) @@ -608,6 +609,7 @@ void QgsExpressionBuilderWidget::updateFunctionTree() registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<>" ), QStringLiteral( " <> " ) ); registerItem( QStringLiteral( "Operators" ), QStringLiteral( "<=" ), QStringLiteral( " <= " ) ); registerItem( QStringLiteral( "Operators" ), QStringLiteral( ">=" ), QStringLiteral( " >= " ) ); + registerItem( QStringLiteral( "Operators" ), QStringLiteral( "[]" ), QStringLiteral( "[ ]" ) ); registerItem( QStringLiteral( "Operators" ), QStringLiteral( "||" ), QStringLiteral( " || " ) ); registerItem( QStringLiteral( "Operators" ), QStringLiteral( "IN" ), QStringLiteral( " IN " ) ); registerItem( QStringLiteral( "Operators" ), QStringLiteral( "LIKE" ), QStringLiteral( " LIKE " ) ); @@ -957,6 +959,10 @@ void QgsExpressionBuilderWidget::createMarkers( const QgsExpressionNode *inNode } break; } + case QgsExpressionNode::NodeType::ntIndexOperator: + { + break; + } } } diff --git a/src/providers/ogr/qgsogrexpressioncompiler.cpp b/src/providers/ogr/qgsogrexpressioncompiler.cpp index d34525bd78f..aab41504485 100644 --- a/src/providers/ogr/qgsogrexpressioncompiler.cpp +++ b/src/providers/ogr/qgsogrexpressioncompiler.cpp @@ -79,6 +79,7 @@ QgsSqlExpressionCompiler::Result QgsOgrExpressionCompiler::compileNode( const Qg case QgsExpressionNode::ntColumnRef: case QgsExpressionNode::ntInOperator: case QgsExpressionNode::ntLiteral: + case QgsExpressionNode::ntIndexOperator: break; } diff --git a/tests/src/core/testqgsexpression.cpp b/tests/src/core/testqgsexpression.cpp index 49266ff8d55..fba08c9dd32 100644 --- a/tests/src/core/testqgsexpression.cpp +++ b/tests/src/core/testqgsexpression.cpp @@ -3181,6 +3181,69 @@ class TestQgsExpression: public QObject QCOMPARE( v.toDateTime().toMSecsSinceEpoch(), v2.toDateTime().toMSecsSinceEpoch() ); } + + void test_IndexOperator() + { + QgsExpressionContext context; + QgsExpression e( QStringLiteral( "'['" ) ); + QVariant result = e.evaluate( &context ); + QCOMPARE( result.toString(), QStringLiteral( "[" ) ); + e = QgsExpression( QStringLiteral( "']'" ) ); + QCOMPARE( e.evaluate( &context ).toString(), QStringLiteral( "]" ) ); + e = QgsExpression( QStringLiteral( "'[3]'" ) ); + QCOMPARE( e.evaluate( &context ).toString(), QStringLiteral( "[3]" ) ); + e = QgsExpression( QStringLiteral( "'a[3]'" ) ); + QCOMPARE( e.evaluate( &context ).toString(), QStringLiteral( "a[3]" ) ); + e = QgsExpression( QStringLiteral( "\"a[3]\"" ) ); + QCOMPARE( e.evaluate( &context ).toString(), QStringLiteral( "[a[3]]" ) ); + e = QgsExpression( QStringLiteral( "(1+2)[0]" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( e.hasEvalError() ); + e = QgsExpression( QStringLiteral( "(1+2)['a']" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( e.hasEvalError() ); + // arrays + e = QgsExpression( QStringLiteral( "array(1,2,3)[0]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 1 ); + e = QgsExpression( QStringLiteral( "((array(1,2,3)))[0]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 1 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[1]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 2 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[2]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 3 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[-1]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 3 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[-2]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 2 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[-3]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 1 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[1+1]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 3 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[(3-2)]" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 2 ); + e = QgsExpression( QStringLiteral( "array(1,2,3)[3]" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( !e.hasEvalError() ); // no eval error - we are tolerant to this + e = QgsExpression( QStringLiteral( "array(1,2,3)[-4]" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( !e.hasEvalError() ); // no eval error - we are tolerant to this + + // maps + e = QgsExpression( QStringLiteral( "map('a',1,'b',2,'c',3)[0]" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( !e.hasEvalError() ); // no eval error - we are tolerant to this + e = QgsExpression( QStringLiteral( "map('a',1,'b',2,'c',3)['d']" ) ); + QVERIFY( !e.evaluate( &context ).isValid() ); + QVERIFY( !e.hasEvalError() ); // no eval error - we are tolerant to this + e = QgsExpression( QStringLiteral( "map('a',1,'b',2,'c',3)['a']" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 1 ); + e = QgsExpression( QStringLiteral( "map('a',1,'b',2,'c',3)['b']" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 2 ); + e = QgsExpression( QStringLiteral( "map('a',1,'b',2,'c',3)['c']" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 3 ); + e = QgsExpression( QStringLiteral( "map('a',1,'bbb',2,'c',3)['b'||'b'||'b']" ) ); + QCOMPARE( e.evaluate( &context ).toInt(), 2 ); + } }; QGSTEST_MAIN( TestQgsExpression ) diff --git a/tests/testdata/qgis_server_accesscontrol/geo.gpkg b/tests/testdata/qgis_server_accesscontrol/geo.gpkg index 274e0b4f4b4cbedece044e4a0a493ce02d362046..5fbc2614d2eee2cf2c932dec23cba9bf65a9debe 100644 GIT binary patch delta 22 dcmZp8z|!!5Wr8%L#6%fqMv2CRtqF_^^Z{DV2V4LE delta 22 dcmZp8z|!!5Wr8%L*hCp;MzO|(tqF_^^Z{C=2Uq|A