Skip to content

Commit

Permalink
Merge pull request #3863 from rldhont/postgres-compile-expression-fun…
Browse files Browse the repository at this point in the history
…ctions

[Feature][PostgreSQL] Compile expression functions
  • Loading branch information
rldhont authored Jan 2, 2017
2 parents cd970f2 + 4bc06b1 commit f59acad
Show file tree
Hide file tree
Showing 9 changed files with 462 additions and 7 deletions.
49 changes: 48 additions & 1 deletion src/core/qgssqlexpressioncompiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -314,18 +314,65 @@ QgsSqlExpressionCompiler::Result QgsSqlExpressionCompiler::compileNode( const Qg
if ( rn != Complete && rn != Partial )
return rn;

result = QStringLiteral( "%1 %2IN(%3)" ).arg( nd, n->isNotIn() ? "NOT " : "", list.join( QStringLiteral( "," ) ) );
result = QStringLiteral( "%1 %2IN (%3)" ).arg( nd, n->isNotIn() ? "NOT " : "", list.join( QStringLiteral( "," ) ) );
return ( inResult == Partial || rn == Partial ) ? Partial : Complete;
}

case QgsExpression::ntFunction:
{
const QgsExpression::NodeFunction* n = static_cast<const QgsExpression::NodeFunction*>( node );
QgsExpression::Function* fd = QgsExpression::Functions()[n->fnIndex()];

// get sql function to compile node expression
QString nd = sqlFunctionFromFunctionName( fd->name() );
// if no sql function the node can't be compiled
if ( nd.isEmpty() )
return Fail;

// compile arguments
QStringList args;
Result inResult = Complete;
Q_FOREACH ( const QgsExpression::Node* ln, n->args()->list() )
{
QString s;
Result r = compileNode( ln, s );
if ( r == Complete || r == Partial )
{
args << s;
if ( r == Partial )
inResult = Partial;
}
else
return r;
}

// update arguments to be adapted to SQL function
args = sqlArgumentsFromFunctionName( fd->name(), args );

// build result
result = QStringLiteral( "%1(%2)" ).arg( nd, args.join( ',' ) );
return inResult == Partial ? Partial : Complete;
}

case QgsExpression::ntCondition:
break;
}

return Fail;
}

QString QgsSqlExpressionCompiler::sqlFunctionFromFunctionName( const QString& fnName ) const
{
Q_UNUSED( fnName );
return QString();
}

QStringList QgsSqlExpressionCompiler::sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const
{
Q_UNUSED( fnName );
return QStringList( fnArgs );
}

bool QgsSqlExpressionCompiler::nodeIsNullLiteral( const QgsExpression::Node* node ) const
{
if ( node->nodeType() != QgsExpression::ntLiteral )
Expand Down
15 changes: 15 additions & 0 deletions src/core/qgssqlexpressioncompiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,21 @@ class CORE_EXPORT QgsSqlExpressionCompiler
*/
virtual Result compileNode( const QgsExpression::Node* node, QString& str );

/** Return the SQL function for the expression function.
* Derived classes should override this to help compile functions
* @param fnName expression function name
* @returns the SQL function name
*/
virtual QString sqlFunctionFromFunctionName( const QString& fnName ) const;

/** Return the Arguments for SQL function for the expression function.
* Derived classes should override this to help compile functions
* @param fnName expression function name
* @param fnArgs arguments from expression
* @returns the arguments updated for SQL Function
*/
virtual QStringList sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const;

QString mResult;
QgsFields mFields;

Expand Down
153 changes: 153 additions & 0 deletions src/providers/postgres/qgspostgresexpressioncompiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@

QgsPostgresExpressionCompiler::QgsPostgresExpressionCompiler( QgsPostgresFeatureSource* source )
: QgsSqlExpressionCompiler( source->mFields )
, mGeometryColumn( source->mGeometryColumn )
, mSpatialColType( source->mSpatialColType )
, mDetectedGeomType( source->mDetectedGeomType )
, mRequestedGeomType( source->mRequestedGeomType )
, mRequestedSrid( source->mRequestedSrid )
, mDetectedSrid( source->mDetectedSrid )
{
}

Expand All @@ -32,3 +38,150 @@ QString QgsPostgresExpressionCompiler::quotedValue( const QVariant& value, bool&
return QgsPostgresConn::quotedValue( value );
}

static const QMap<QString, QString>& functionNamesSqlFunctionsMap()
{
static QMap<QString, QString> fnNames;
if ( fnNames.isEmpty() )
{
fnNames =
{
{ "sqrt", "sqrt" },
{ "radians", "radians" },
{ "degrees", "degrees" },
{ "abs", "abs" },
{ "cos", "cos" },
{ "sin", "sin" },
{ "tan", "tan" },
{ "acos", "acos" },
{ "asin", "asin" },
{ "atan", "atan" },
{ "atan2", "atan2" },
{ "exp", "exp" },
{ "ln", "ln" },
{ "log", "log" },
{ "log10", "log" },
{ "round", "round" },
{ "floor", "floor" },
{ "ceil", "ceil" },
{ "pi", "pi" },
// geometry functions
//{ "azimuth", "ST_Azimuth" },
{ "x", "ST_X" },
{ "y", "ST_Y" },
//{ "z", "ST_Z" },
//{ "m", "ST_M" },
{ "x_min", "ST_XMin" },
{ "y_min", "ST_YMin" },
{ "x_max", "ST_XMax" },
{ "y_max", "ST_YMax" },
{ "area", "ST_Area" },
{ "perimeter", "ST_Perimeter" },
{ "relate", "ST_Relate" },
{ "disjoint", "ST_Disjoint" },
{ "intersects", "ST_Intersects" },
//{ "touches", "ST_Touches" },
{ "crosses", "ST_Crosses" },
{ "contains", "ST_Contains" },
{ "overlaps", "ST_Overlaps" },
{ "within", "ST_Within" },
{ "translate", "ST_Translate" },
{ "buffer", "ST_Buffer" },
{ "centroid", "ST_Centroid" },
{ "point_on_surface", "ST_PointOnSurface" },
//{ "reverse", "ST_Reverse" },
//{ "is_closed", "ST_IsClosed" },
//{ "convex_hull", "ST_ConvexHull" },
//{ "difference", "ST_Difference" },
{ "distance", "ST_Distance" },
//{ "intersection", "ST_Intersection" },
//{ "sym_difference", "ST_SymDifference" },
//{ "combine", "ST_Union" },
//{ "union", "ST_Union" },
{ "geom_from_wkt", "ST_GeomFromText" },
{ "geom_from_gml", "ST_GeomFromGML" }
};
}
return fnNames;
}

QString QgsPostgresExpressionCompiler::sqlFunctionFromFunctionName( const QString& fnName ) const
{
return functionNamesSqlFunctionsMap().value( fnName, QString() );
}

QStringList QgsPostgresExpressionCompiler::sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const
{
QStringList args( fnArgs );
if ( fnName == "geom_from_wkt" )
{
args << ( mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
}
else if ( fnName == "geom_from_gml" )
{
args << ( mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
}
else if ( fnName == "x" || fnName == "y" )
{
args = QStringList( QStringLiteral( "ST_Centroid(%1)" ).arg( args[0] ) );
}
else if ( fnName == "buffer" && args.length() == 2 )
{
args << "8";
}
// x and y functions have to be adapted
return args;
}

QgsSqlExpressionCompiler::Result QgsPostgresExpressionCompiler::compileNode( const QgsExpression::Node* node, QString& result )
{
switch ( node->nodeType() )
{
case QgsExpression::ntFunction:
{
const QgsExpression::NodeFunction* n = static_cast<const QgsExpression::NodeFunction*>( node );

QgsExpression::Function* fd = QgsExpression::Functions()[n->fnIndex()];
if ( fd->name() == "$geometry" )
{
result = quotedIdentifier( mGeometryColumn );
return Complete;
}
/*
* These methods are tricky
* QGIS expression versions of these return ellipsoidal measurements
* based on the project settings, and also convert the result to the
* units specified in project properties.
else if ( fd->name() == "$area" )
{
result = QStringLiteral( "ST_Area(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
return Complete;
}
else if ( fd->name() == "$length" )
{
result = QStringLiteral( "ST_Length(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
return Complete;
}
else if ( fd->name() == "$perimeter" )
{
result = QStringLiteral( "ST_Perimeter(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
return Complete;
}
else if ( fd->name() == "$x" )
{
result = QStringLiteral( "ST_X(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
return Complete;
}
else if ( fd->name() == "$y" )
{
result = QStringLiteral( "ST_Y(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
return Complete;
}
*/
}

default:
return QgsSqlExpressionCompiler::compileNode( node, result );
}

return Fail;
}
11 changes: 11 additions & 0 deletions src/providers/postgres/qgspostgresexpressioncompiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "qgssqlexpressioncompiler.h"
#include "qgsexpression.h"
#include "qgspostgresconn.h"
#include "qgspostgresfeatureiterator.h"

class QgsPostgresExpressionCompiler : public QgsSqlExpressionCompiler
Expand All @@ -30,6 +31,16 @@ class QgsPostgresExpressionCompiler : public QgsSqlExpressionCompiler

virtual QString quotedIdentifier( const QString& identifier ) override;
virtual QString quotedValue( const QVariant& value, bool& ok ) override;
virtual Result compileNode( const QgsExpression::Node* node, QString& str ) override;
virtual QString sqlFunctionFromFunctionName( const QString& fnName ) const override;
virtual QStringList sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const override;

QString mGeometryColumn;
QgsPostgresGeometryColumnType mSpatialColType;
QgsWkbTypes::Type mDetectedGeomType;
QgsWkbTypes::Type mRequestedGeomType;
QString mRequestedSrid;
QString mDetectedSrid;
};

#endif // QGSPOSTGRESEXPRESSIONCOMPILER_H
68 changes: 67 additions & 1 deletion tests/src/python/providertestbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,8 +222,47 @@ def runGetFeatureTests(self, provider):
# against numeric literals
self.assert_query(provider, 'num_char IN (2, 4, 5)', [2, 4, 5])

#function
self.assert_query(provider, 'sqrt(pk) >= 2', [4, 5])
self.assert_query(provider, 'radians(cnt) < 2', [1, 5])
self.assert_query(provider, 'degrees(pk) <= 200', [1, 2, 3])
self.assert_query(provider, 'abs(cnt) <= 200', [1, 2, 5])
self.assert_query(provider, 'cos(pk) < 0', [2, 3, 4])
self.assert_query(provider, 'sin(pk) < 0', [4, 5])
self.assert_query(provider, 'tan(pk) < 0', [2, 3, 5])
self.assert_query(provider, 'acos(-1) < pk', [4, 5])
self.assert_query(provider, 'asin(1) < pk', [2, 3, 4, 5])
self.assert_query(provider, 'atan(3.14) < pk', [2, 3, 4, 5])
self.assert_query(provider, 'atan2(3.14, pk) < 1', [3, 4, 5])
self.assert_query(provider, 'exp(pk) < 10', [1, 2])
self.assert_query(provider, 'ln(pk) <= 1', [1, 2])
self.assert_query(provider, 'log(3, pk) <= 1', [1, 2, 3])
self.assert_query(provider, 'log10(pk) < 0.5', [1, 2, 3])
self.assert_query(provider, 'round(3.14) <= pk', [3, 4, 5])
self.assert_query(provider, 'floor(3.14) <= pk', [3, 4, 5])
self.assert_query(provider, 'ceil(3.14) <= pk', [4, 5])
self.assert_query(provider, 'pk < pi()', [1, 2, 3])

self.assert_query(provider, 'round(cnt / 66.67) <= 2', [1, 5])
self.assert_query(provider, 'floor(cnt / 66.67) <= 2', [1, 2, 5])
self.assert_query(provider, 'ceil(cnt / 66.67) <= 2', [1, 5])
self.assert_query(provider, 'pk < pi() / 2', [1])

# geometry
# azimuth and touches tests are deactivated because they do not pass for WFS provider
#self.assert_query(provider, 'azimuth($geometry,geom_from_wkt( \'Point (-70 70)\')) < pi()', [1, 5])
self.assert_query(provider, 'x($geometry) < -70', [1, 5])
self.assert_query(provider, 'y($geometry) > 70', [2, 4, 5])
self.assert_query(provider, 'xmin($geometry) < -70', [1, 5])
self.assert_query(provider, 'ymin($geometry) > 70', [2, 4, 5])
self.assert_query(provider, 'xmax($geometry) < -70', [1, 5])
self.assert_query(provider, 'ymax($geometry) > 70', [2, 4, 5])
self.assert_query(provider, 'disjoint($geometry,geom_from_wkt( \'Polygon ((-72.2 66.1, -65.2 66.1, -65.2 72.0, -72.2 72.0, -72.2 66.1))\'))', [4, 5])
self.assert_query(provider, 'intersects($geometry,geom_from_wkt( \'Polygon ((-72.2 66.1, -65.2 66.1, -65.2 72.0, -72.2 72.0, -72.2 66.1))\'))', [1, 2])
#self.assert_query(provider, 'touches($geometry,geom_from_wkt( \'Polygon ((-70.332 66.33, -65.32 66.33, -65.32 78.3, -70.332 78.3, -70.332 66.33))\'))', [1, 4])
self.assert_query(provider, 'contains(geom_from_wkt( \'Polygon ((-72.2 66.1, -65.2 66.1, -65.2 72.0, -72.2 72.0, -72.2 66.1))\'),$geometry)', [1, 2])
self.assert_query(provider, 'distance($geometry,geom_from_wkt( \'Point (-70 70)\')) > 7', [4, 5])
self.assert_query(provider, 'intersects($geometry,geom_from_gml( \'<gml:Polygon srsName="EPSG:4326"><gml:outerBoundaryIs><gml:LinearRing><gml:coordinates>-72.2,66.1 -65.2,66.1 -65.2,72.0 -72.2,72.0 -72.2,66.1</gml:coordinates></gml:LinearRing></gml:outerBoundaryIs></gml:Polygon>\'))', [1, 2])

# combination of an uncompilable expression and limit
feature = next(self.vl.getFeatures('pk=4'))
Expand All @@ -240,19 +279,46 @@ def runGetFeatureTests(self, provider):
values = [f['pk'] for f in self.vl.getFeatures(request)]
self.assertEqual(values, [4])

def runPolyGetFeatureTests(self, provider):
assert len([f for f in provider.getFeatures()]) == 4

# geometry
self.assert_query(provider, 'x($geometry) < -70', [1])
self.assert_query(provider, 'y($geometry) > 79', [1, 2])
self.assert_query(provider, 'xmin($geometry) < -70', [1, 3])
self.assert_query(provider, 'ymin($geometry) < 76', [3])
self.assert_query(provider, 'xmax($geometry) > -68', [2, 3])
self.assert_query(provider, 'ymax($geometry) > 80', [1, 2])
self.assert_query(provider, 'area($geometry) > 10', [1])
self.assert_query(provider, 'perimeter($geometry) < 12', [2, 3])
self.assert_query(provider, 'relate($geometry,geom_from_wkt( \'Polygon ((-68.2 82.1, -66.95 82.1, -66.95 79.05, -68.2 79.05, -68.2 82.1))\')) = \'FF2FF1212\'', [1, 3])
self.assert_query(provider, 'relate($geometry,geom_from_wkt( \'Polygon ((-68.2 82.1, -66.95 82.1, -66.95 79.05, -68.2 79.05, -68.2 82.1))\'), \'****F****\')', [1, 3])
self.assert_query(provider, 'crosses($geometry,geom_from_wkt( \'Linestring (-68.2 82.1, -66.95 82.1, -66.95 79.05)\'))', [2])
self.assert_query(provider, 'overlaps($geometry,geom_from_wkt( \'Polygon ((-68.2 82.1, -66.95 82.1, -66.95 79.05, -68.2 79.05, -68.2 82.1))\'))', [2])
self.assert_query(provider, 'within($geometry,geom_from_wkt( \'Polygon ((-75.1 76.1, -75.1 81.6, -68.8 81.6, -68.8 76.1, -75.1 76.1))\'))', [1])
self.assert_query(provider, 'overlaps(translate($geometry,-1,-1),geom_from_wkt( \'Polygon ((-75.1 76.1, -75.1 81.6, -68.8 81.6, -68.8 76.1, -75.1 76.1))\'))', [1])
self.assert_query(provider, 'overlaps(buffer($geometry,1),geom_from_wkt( \'Polygon ((-75.1 76.1, -75.1 81.6, -68.8 81.6, -68.8 76.1, -75.1 76.1))\'))', [1, 3])
self.assert_query(provider, 'intersects(centroid($geometry),geom_from_wkt( \'Polygon ((-74.4 78.2, -74.4 79.1, -66.8 79.1, -66.8 78.2, -74.4 78.2))\'))', [2])
self.assert_query(provider, 'intersects(point_on_surface($geometry),geom_from_wkt( \'Polygon ((-74.4 78.2, -74.4 79.1, -66.8 79.1, -66.8 78.2, -74.4 78.2))\'))', [1, 2])
self.assert_query(provider, 'distance($geometry,geom_from_wkt( \'Point (-70 70)\')) > 7', [1, 2])

def testGetFeaturesUncompiled(self):
self.compiled = False
try:
self.disableCompiler()
except AttributeError:
pass
self.runGetFeatureTests(self.provider)
if hasattr(self, 'poly_provider'):
self.runPolyGetFeatureTests(self.poly_provider)

def testGetFeaturesCompiled(self):
def testPolyGetFeaturesCompiled(self):
try:
self.enableCompiler()
self.compiled = True
self.runGetFeatureTests(self.provider)
if hasattr(self, 'poly_provider'):
self.runPolyGetFeatureTests(self.poly_provider)
except AttributeError:
print('Provider does not support compiling')

Expand Down
Loading

0 comments on commit f59acad

Please sign in to comment.