Skip to content

Commit

Permalink
Extend QgsSQLStatement to allow parsing of fragments of SQL clauses,
Browse files Browse the repository at this point in the history
specifically to allow parsing an expression or where clause alone
  • Loading branch information
nyalldawson committed Aug 9, 2020
1 parent 370b01b commit 8c9e7c1
Show file tree
Hide file tree
Showing 5 changed files with 127 additions and 19 deletions.
26 changes: 24 additions & 2 deletions python/core/auto_generated/qgssqlstatement.sip.in
Expand Up @@ -9,7 +9,6 @@




class QgsSQLStatement
{
%Docstring
Expand All @@ -33,7 +32,7 @@ Creates a new statement based on the provided string.
Create a copy of this statement.
%End

~QgsSQLStatement();
virtual ~QgsSQLStatement();

bool hasParserError() const;
%Docstring
Expand Down Expand Up @@ -1000,6 +999,29 @@ Entry function for the visitor pattern
%End

protected:

QgsSQLStatement( const QString &statement, bool allowFragments );
};


class QgsSQLStatementFragment : QgsSQLStatement
{
%Docstring
Class for parsing fragments of SQL statements, such as an expression or where clause.

.. versionadded:: 3.16
%End

%TypeHeaderCode
#include "qgssqlstatement.h"
%End
public:

QgsSQLStatementFragment( const QString &fragment );
%Docstring
Constructor for QgsSQLStatementFragment of the specified ``fragment``.
%End

};


Expand Down
26 changes: 21 additions & 5 deletions src/core/qgssqlstatement.cpp
Expand Up @@ -22,7 +22,7 @@


// from parser
extern QgsSQLStatement::Node *parse( const QString &str, QString &parserErrorMsg );
extern QgsSQLStatement::Node *parse( const QString &str, QString &parserErrorMsg, bool allowFragments );

///////////////////////////////////////////////
// operators
Expand Down Expand Up @@ -116,14 +116,20 @@ QString QgsSQLStatement::quotedString( QString text )
}

QgsSQLStatement::QgsSQLStatement( const QString &expr )
: QgsSQLStatement( expr, false )
{
mRootNode = ::parse( expr, mParserErrorString );
}

QgsSQLStatement::QgsSQLStatement( const QString &expr, bool allowFragments )
: mAllowFragments( allowFragments )
{
mRootNode = ::parse( expr, mParserErrorString, mAllowFragments );
mStatement = expr;
}

QgsSQLStatement::QgsSQLStatement( const QgsSQLStatement &other )
{
mRootNode = ::parse( other.mStatement, mParserErrorString );
mRootNode = ::parse( other.mStatement, mParserErrorString, other.mAllowFragments );
mStatement = other.mStatement;
}

Expand All @@ -133,7 +139,7 @@ QgsSQLStatement &QgsSQLStatement::operator=( const QgsSQLStatement &other )
{
delete mRootNode;
mParserErrorString.clear();
mRootNode = ::parse( other.mStatement, mParserErrorString );
mRootNode = ::parse( other.mStatement, mParserErrorString, other.mAllowFragments );
mStatement = other.mStatement;
}
return *this;
Expand All @@ -144,7 +150,7 @@ QgsSQLStatement::~QgsSQLStatement()
delete mRootNode;
}

bool QgsSQLStatement::hasParserError() const { return !mParserErrorString.isNull(); }
bool QgsSQLStatement::hasParserError() const { return !mParserErrorString.isNull() || ( !mRootNode && !mAllowFragments ); }

QString QgsSQLStatement::parserErrorString() const { return mParserErrorString; }

Expand Down Expand Up @@ -743,3 +749,13 @@ QgsSQLStatement::Node *QgsSQLStatement::NodeCast::clone() const
{
return new NodeCast( mNode->clone(), mType );
}

//
// QgsSQLStatementFragment
//

QgsSQLStatementFragment::QgsSQLStatementFragment( const QString &fragment )
: QgsSQLStatement( fragment, true )
{

}
25 changes: 22 additions & 3 deletions src/core/qgssqlstatement.h
Expand Up @@ -29,10 +29,9 @@

/**
* \ingroup core
Class for parsing SQL statements.
Class for parsing SQL statements.
* \since QGIS 2.16
*/

class CORE_EXPORT QgsSQLStatement
{
Q_DECLARE_TR_FUNCTIONS( QgsSQLStatement )
Expand All @@ -52,7 +51,7 @@ class CORE_EXPORT QgsSQLStatement
* Create a copy of this statement.
*/
QgsSQLStatement &operator=( const QgsSQLStatement &other );
~QgsSQLStatement();
virtual ~QgsSQLStatement();

//! Returns TRUE if an error occurred when parsing the input statement
bool hasParserError() const;
Expand Down Expand Up @@ -830,10 +829,30 @@ class CORE_EXPORT QgsSQLStatement

protected:
QgsSQLStatement::Node *mRootNode = nullptr;
bool mAllowFragments = false;
QString mStatement;
QString mParserErrorString;

QgsSQLStatement( const QString &statement, bool allowFragments );
};

Q_DECLARE_METATYPE( QgsSQLStatement::Node * )

/**
* \ingroup core
* Class for parsing fragments of SQL statements, such as an expression or where clause.
* \since QGIS 3.16
*/
class CORE_EXPORT QgsSQLStatementFragment : public QgsSQLStatement
{
public:

/**
* Constructor for QgsSQLStatementFragment of the specified \a fragment.
*/
QgsSQLStatementFragment( const QString &fragment );

};


#endif // QGSSQLSTATEMENT_H
27 changes: 19 additions & 8 deletions src/core/qgssqlstatementparser.yy
Expand Up @@ -60,11 +60,13 @@ struct sqlstatement_parser_context

QgsSQLStatement::Node* whereExp;

QgsSQLStatement::Node* expression;

QList<QgsSQLStatement::NodeJoin*> joinList;

QList<QgsSQLStatement::NodeColumnSorted*> orderByList;

sqlstatement_parser_context() : rootNode( nullptr ), whereExp( nullptr ) {}
sqlstatement_parser_context() : rootNode( nullptr ), whereExp( nullptr ), expression( nullptr ) {}

void setWhere( QgsSQLStatement::Node* whereExp ) { this->whereExp = whereExp; }

Expand Down Expand Up @@ -203,6 +205,7 @@ struct sqlstatement_parser_context
%%

root: select_statement { parser_ctx->rootNode = $1; }
| expr { parser_ctx->expression = $1; }
;

/* We have to separate expr from expr_non_logical to avoid */
Expand Down Expand Up @@ -594,7 +597,7 @@ table_list:


// returns parsed tree, otherwise returns nullptr and sets parserErrorMsg
QgsSQLStatement::Node* parse(const QString& str, QString& parserErrorMsg)
QgsSQLStatement::Node* parse(const QString& str, QString& parserErrorMsg, bool allowFragments )
{
sqlstatement_parser_context ctx;
ctx.rootNode = 0;
Expand All @@ -605,16 +608,24 @@ QgsSQLStatement::Node* parse(const QString& str, QString& parserErrorMsg)
sqlstatement_lex_destroy(ctx.flex_scanner);

// list should be empty when parsing was OK
if (res == 0) // success?
if ( res == 0 && ( ctx.rootNode || ( allowFragments && ctx.expression ) ) ) // success?
{
ctx.rootNode->setWhere(ctx.whereExp);
ctx.rootNode->setJoins(ctx.joinList);
ctx.rootNode->setOrderBy(ctx.orderByList);
return ctx.rootNode;
if ( ctx.rootNode )
{
ctx.rootNode->setWhere(ctx.whereExp);
ctx.rootNode->setJoins(ctx.joinList);
ctx.rootNode->setOrderBy(ctx.orderByList);
return ctx.rootNode;
}
else
{
//fragment
return ctx.expression;
}
}
else // error?
{
parserErrorMsg = ctx.errorMsg;
parserErrorMsg = !allowFragments && !ctx.rootNode ? QStringLiteral("Expression must begin with SELECT") : ctx.errorMsg;
delete ctx.rootNode;
delete ctx.whereExp;
qDeleteAll(ctx.joinList);
Expand Down
42 changes: 41 additions & 1 deletion tests/src/python/test_qgssqlstatement.py
Expand Up @@ -11,7 +11,7 @@
__copyright__ = 'Copyright 2016, The QGIS Project'

from qgis.testing import unittest
from qgis.core import QgsSQLStatement
from qgis.core import QgsSQLStatement, QgsSQLStatementFragment


class TestQgsSQLStatementCustomFunctions(unittest.TestCase):
Expand Down Expand Up @@ -178,6 +178,46 @@ def testBasicValidationCheck(self):
self.assertEqual(
errorMsg, 'Table t_unknown is referenced by column c, but not selected in FROM / JOIN.')

def testFragmentColumnRef(self):
exp = QgsSQLStatementFragment('col')
self.assertFalse(exp.hasParserError())
self.assertIsInstance(exp.rootNode(), QgsSQLStatement.NodeColumnRef)
self.assertEqual(exp.rootNode().name(), 'col')

exp = QgsSQLStatementFragment('"col"')
self.assertFalse(exp.hasParserError())
self.assertIsInstance(exp.rootNode(), QgsSQLStatement.NodeColumnRef)
self.assertEqual(exp.rootNode().name(), 'col')

def testFragmentFunction(self):
exp = QgsSQLStatementFragment('upper(col)')
self.assertFalse(exp.hasParserError())
self.assertIsInstance(exp.rootNode(), QgsSQLStatement.NodeFunction)
self.assertEqual(exp.rootNode().name(), 'upper')

def testFragmentCondition(self):
exp = QgsSQLStatementFragment('col = \'a\'')
self.assertFalse(exp.hasParserError())
self.assertIsInstance(exp.rootNode(), QgsSQLStatement.NodeBinaryOperator)
self.assertEqual(exp.rootNode().opLeft().name(), 'col')
self.assertEqual(exp.rootNode().opRight().value(), 'a')

def checkFragmentError(self, statement):
exp = QgsSQLStatementFragment(statement)
self.assertEqual(exp.hasParserError(), True)
self.assertNotEqual(exp.parserErrorString(), '')
self.assertEqual(exp.dump(), "(no root)")
self.assertEqual(exp.rootNode(), None)

def testFragmentError(self):
self.checkFragmentError("SELECT")
self.checkFragmentError("SELECT a")
self.checkFragmentError("SELECT a, FROM b")
self.checkFragmentError("=")
self.checkFragmentError("WHERE 1")
self.checkFragmentError("FROM b")
self.checkFragmentError("ORDER BY a")


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

0 comments on commit 8c9e7c1

Please sign in to comment.