Skip to content

Commit f59acad

Browse files
authored
Merge pull request #3863 from rldhont/postgres-compile-expression-functions
[Feature][PostgreSQL] Compile expression functions
2 parents cd970f2 + 4bc06b1 commit f59acad

9 files changed

Lines changed: 462 additions & 7 deletions

‎src/core/qgssqlexpressioncompiler.cpp‎

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,18 +314,65 @@ QgsSqlExpressionCompiler::Result QgsSqlExpressionCompiler::compileNode( const Qg
314314
if ( rn != Complete && rn != Partial )
315315
return rn;
316316

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

321321
case QgsExpression::ntFunction:
322+
{
323+
const QgsExpression::NodeFunction* n = static_cast<const QgsExpression::NodeFunction*>( node );
324+
QgsExpression::Function* fd = QgsExpression::Functions()[n->fnIndex()];
325+
326+
// get sql function to compile node expression
327+
QString nd = sqlFunctionFromFunctionName( fd->name() );
328+
// if no sql function the node can't be compiled
329+
if ( nd.isEmpty() )
330+
return Fail;
331+
332+
// compile arguments
333+
QStringList args;
334+
Result inResult = Complete;
335+
Q_FOREACH ( const QgsExpression::Node* ln, n->args()->list() )
336+
{
337+
QString s;
338+
Result r = compileNode( ln, s );
339+
if ( r == Complete || r == Partial )
340+
{
341+
args << s;
342+
if ( r == Partial )
343+
inResult = Partial;
344+
}
345+
else
346+
return r;
347+
}
348+
349+
// update arguments to be adapted to SQL function
350+
args = sqlArgumentsFromFunctionName( fd->name(), args );
351+
352+
// build result
353+
result = QStringLiteral( "%1(%2)" ).arg( nd, args.join( ',' ) );
354+
return inResult == Partial ? Partial : Complete;
355+
}
356+
322357
case QgsExpression::ntCondition:
323358
break;
324359
}
325360

326361
return Fail;
327362
}
328363

364+
QString QgsSqlExpressionCompiler::sqlFunctionFromFunctionName( const QString& fnName ) const
365+
{
366+
Q_UNUSED( fnName );
367+
return QString();
368+
}
369+
370+
QStringList QgsSqlExpressionCompiler::sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const
371+
{
372+
Q_UNUSED( fnName );
373+
return QStringList( fnArgs );
374+
}
375+
329376
bool QgsSqlExpressionCompiler::nodeIsNullLiteral( const QgsExpression::Node* node ) const
330377
{
331378
if ( node->nodeType() != QgsExpression::ntLiteral )

‎src/core/qgssqlexpressioncompiler.h‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ class CORE_EXPORT QgsSqlExpressionCompiler
9393
*/
9494
virtual Result compileNode( const QgsExpression::Node* node, QString& str );
9595

96+
/** Return the SQL function for the expression function.
97+
* Derived classes should override this to help compile functions
98+
* @param fnName expression function name
99+
* @returns the SQL function name
100+
*/
101+
virtual QString sqlFunctionFromFunctionName( const QString& fnName ) const;
102+
103+
/** Return the Arguments for SQL function for the expression function.
104+
* Derived classes should override this to help compile functions
105+
* @param fnName expression function name
106+
* @param fnArgs arguments from expression
107+
* @returns the arguments updated for SQL Function
108+
*/
109+
virtual QStringList sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const;
110+
96111
QString mResult;
97112
QgsFields mFields;
98113

‎src/providers/postgres/qgspostgresexpressioncompiler.cpp‎

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
QgsPostgresExpressionCompiler::QgsPostgresExpressionCompiler( QgsPostgresFeatureSource* source )
2020
: QgsSqlExpressionCompiler( source->mFields )
21+
, mGeometryColumn( source->mGeometryColumn )
22+
, mSpatialColType( source->mSpatialColType )
23+
, mDetectedGeomType( source->mDetectedGeomType )
24+
, mRequestedGeomType( source->mRequestedGeomType )
25+
, mRequestedSrid( source->mRequestedSrid )
26+
, mDetectedSrid( source->mDetectedSrid )
2127
{
2228
}
2329

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

41+
static const QMap<QString, QString>& functionNamesSqlFunctionsMap()
42+
{
43+
static QMap<QString, QString> fnNames;
44+
if ( fnNames.isEmpty() )
45+
{
46+
fnNames =
47+
{
48+
{ "sqrt", "sqrt" },
49+
{ "radians", "radians" },
50+
{ "degrees", "degrees" },
51+
{ "abs", "abs" },
52+
{ "cos", "cos" },
53+
{ "sin", "sin" },
54+
{ "tan", "tan" },
55+
{ "acos", "acos" },
56+
{ "asin", "asin" },
57+
{ "atan", "atan" },
58+
{ "atan2", "atan2" },
59+
{ "exp", "exp" },
60+
{ "ln", "ln" },
61+
{ "log", "log" },
62+
{ "log10", "log" },
63+
{ "round", "round" },
64+
{ "floor", "floor" },
65+
{ "ceil", "ceil" },
66+
{ "pi", "pi" },
67+
// geometry functions
68+
//{ "azimuth", "ST_Azimuth" },
69+
{ "x", "ST_X" },
70+
{ "y", "ST_Y" },
71+
//{ "z", "ST_Z" },
72+
//{ "m", "ST_M" },
73+
{ "x_min", "ST_XMin" },
74+
{ "y_min", "ST_YMin" },
75+
{ "x_max", "ST_XMax" },
76+
{ "y_max", "ST_YMax" },
77+
{ "area", "ST_Area" },
78+
{ "perimeter", "ST_Perimeter" },
79+
{ "relate", "ST_Relate" },
80+
{ "disjoint", "ST_Disjoint" },
81+
{ "intersects", "ST_Intersects" },
82+
//{ "touches", "ST_Touches" },
83+
{ "crosses", "ST_Crosses" },
84+
{ "contains", "ST_Contains" },
85+
{ "overlaps", "ST_Overlaps" },
86+
{ "within", "ST_Within" },
87+
{ "translate", "ST_Translate" },
88+
{ "buffer", "ST_Buffer" },
89+
{ "centroid", "ST_Centroid" },
90+
{ "point_on_surface", "ST_PointOnSurface" },
91+
//{ "reverse", "ST_Reverse" },
92+
//{ "is_closed", "ST_IsClosed" },
93+
//{ "convex_hull", "ST_ConvexHull" },
94+
//{ "difference", "ST_Difference" },
95+
{ "distance", "ST_Distance" },
96+
//{ "intersection", "ST_Intersection" },
97+
//{ "sym_difference", "ST_SymDifference" },
98+
//{ "combine", "ST_Union" },
99+
//{ "union", "ST_Union" },
100+
{ "geom_from_wkt", "ST_GeomFromText" },
101+
{ "geom_from_gml", "ST_GeomFromGML" }
102+
};
103+
}
104+
return fnNames;
105+
}
106+
107+
QString QgsPostgresExpressionCompiler::sqlFunctionFromFunctionName( const QString& fnName ) const
108+
{
109+
return functionNamesSqlFunctionsMap().value( fnName, QString() );
110+
}
111+
112+
QStringList QgsPostgresExpressionCompiler::sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const
113+
{
114+
QStringList args( fnArgs );
115+
if ( fnName == "geom_from_wkt" )
116+
{
117+
args << ( mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
118+
}
119+
else if ( fnName == "geom_from_gml" )
120+
{
121+
args << ( mRequestedSrid.isEmpty() ? mDetectedSrid : mRequestedSrid );
122+
}
123+
else if ( fnName == "x" || fnName == "y" )
124+
{
125+
args = QStringList( QStringLiteral( "ST_Centroid(%1)" ).arg( args[0] ) );
126+
}
127+
else if ( fnName == "buffer" && args.length() == 2 )
128+
{
129+
args << "8";
130+
}
131+
// x and y functions have to be adapted
132+
return args;
133+
}
134+
135+
QgsSqlExpressionCompiler::Result QgsPostgresExpressionCompiler::compileNode( const QgsExpression::Node* node, QString& result )
136+
{
137+
switch ( node->nodeType() )
138+
{
139+
case QgsExpression::ntFunction:
140+
{
141+
const QgsExpression::NodeFunction* n = static_cast<const QgsExpression::NodeFunction*>( node );
142+
143+
QgsExpression::Function* fd = QgsExpression::Functions()[n->fnIndex()];
144+
if ( fd->name() == "$geometry" )
145+
{
146+
result = quotedIdentifier( mGeometryColumn );
147+
return Complete;
148+
}
149+
/*
150+
* These methods are tricky
151+
* QGIS expression versions of these return ellipsoidal measurements
152+
* based on the project settings, and also convert the result to the
153+
* units specified in project properties.
154+
else if ( fd->name() == "$area" )
155+
{
156+
result = QStringLiteral( "ST_Area(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
157+
return Complete;
158+
}
159+
else if ( fd->name() == "$length" )
160+
{
161+
result = QStringLiteral( "ST_Length(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
162+
return Complete;
163+
}
164+
else if ( fd->name() == "$perimeter" )
165+
{
166+
result = QStringLiteral( "ST_Perimeter(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
167+
return Complete;
168+
}
169+
else if ( fd->name() == "$x" )
170+
{
171+
result = QStringLiteral( "ST_X(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
172+
return Complete;
173+
}
174+
else if ( fd->name() == "$y" )
175+
{
176+
result = QStringLiteral( "ST_Y(%1)" ).arg( quotedIdentifier( mGeometryColumn ) );
177+
return Complete;
178+
}
179+
*/
180+
}
181+
182+
default:
183+
return QgsSqlExpressionCompiler::compileNode( node, result );
184+
}
185+
186+
return Fail;
187+
}

‎src/providers/postgres/qgspostgresexpressioncompiler.h‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
#include "qgssqlexpressioncompiler.h"
2020
#include "qgsexpression.h"
21+
#include "qgspostgresconn.h"
2122
#include "qgspostgresfeatureiterator.h"
2223

2324
class QgsPostgresExpressionCompiler : public QgsSqlExpressionCompiler
@@ -30,6 +31,16 @@ class QgsPostgresExpressionCompiler : public QgsSqlExpressionCompiler
3031

3132
virtual QString quotedIdentifier( const QString& identifier ) override;
3233
virtual QString quotedValue( const QVariant& value, bool& ok ) override;
34+
virtual Result compileNode( const QgsExpression::Node* node, QString& str ) override;
35+
virtual QString sqlFunctionFromFunctionName( const QString& fnName ) const override;
36+
virtual QStringList sqlArgumentsFromFunctionName( const QString& fnName, const QStringList& fnArgs ) const override;
37+
38+
QString mGeometryColumn;
39+
QgsPostgresGeometryColumnType mSpatialColType;
40+
QgsWkbTypes::Type mDetectedGeomType;
41+
QgsWkbTypes::Type mRequestedGeomType;
42+
QString mRequestedSrid;
43+
QString mDetectedSrid;
3344
};
3445

3546
#endif // QGSPOSTGRESEXPRESSIONCOMPILER_H

‎tests/src/python/providertestbase.py‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -222,8 +222,47 @@ def runGetFeatureTests(self, provider):
222222
# against numeric literals
223223
self.assert_query(provider, 'num_char IN (2, 4, 5)', [2, 4, 5])
224224

225+
#function
226+
self.assert_query(provider, 'sqrt(pk) >= 2', [4, 5])
227+
self.assert_query(provider, 'radians(cnt) < 2', [1, 5])
228+
self.assert_query(provider, 'degrees(pk) <= 200', [1, 2, 3])
229+
self.assert_query(provider, 'abs(cnt) <= 200', [1, 2, 5])
230+
self.assert_query(provider, 'cos(pk) < 0', [2, 3, 4])
231+
self.assert_query(provider, 'sin(pk) < 0', [4, 5])
232+
self.assert_query(provider, 'tan(pk) < 0', [2, 3, 5])
233+
self.assert_query(provider, 'acos(-1) < pk', [4, 5])
234+
self.assert_query(provider, 'asin(1) < pk', [2, 3, 4, 5])
235+
self.assert_query(provider, 'atan(3.14) < pk', [2, 3, 4, 5])
236+
self.assert_query(provider, 'atan2(3.14, pk) < 1', [3, 4, 5])
237+
self.assert_query(provider, 'exp(pk) < 10', [1, 2])
238+
self.assert_query(provider, 'ln(pk) <= 1', [1, 2])
239+
self.assert_query(provider, 'log(3, pk) <= 1', [1, 2, 3])
240+
self.assert_query(provider, 'log10(pk) < 0.5', [1, 2, 3])
241+
self.assert_query(provider, 'round(3.14) <= pk', [3, 4, 5])
242+
self.assert_query(provider, 'floor(3.14) <= pk', [3, 4, 5])
243+
self.assert_query(provider, 'ceil(3.14) <= pk', [4, 5])
244+
self.assert_query(provider, 'pk < pi()', [1, 2, 3])
245+
246+
self.assert_query(provider, 'round(cnt / 66.67) <= 2', [1, 5])
247+
self.assert_query(provider, 'floor(cnt / 66.67) <= 2', [1, 2, 5])
248+
self.assert_query(provider, 'ceil(cnt / 66.67) <= 2', [1, 5])
249+
self.assert_query(provider, 'pk < pi() / 2', [1])
250+
225251
# geometry
252+
# azimuth and touches tests are deactivated because they do not pass for WFS provider
253+
#self.assert_query(provider, 'azimuth($geometry,geom_from_wkt( \'Point (-70 70)\')) < pi()', [1, 5])
254+
self.assert_query(provider, 'x($geometry) < -70', [1, 5])
255+
self.assert_query(provider, 'y($geometry) > 70', [2, 4, 5])
256+
self.assert_query(provider, 'xmin($geometry) < -70', [1, 5])
257+
self.assert_query(provider, 'ymin($geometry) > 70', [2, 4, 5])
258+
self.assert_query(provider, 'xmax($geometry) < -70', [1, 5])
259+
self.assert_query(provider, 'ymax($geometry) > 70', [2, 4, 5])
260+
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])
226261
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])
262+
#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])
263+
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])
264+
self.assert_query(provider, 'distance($geometry,geom_from_wkt( \'Point (-70 70)\')) > 7', [4, 5])
265+
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])
227266

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

282+
def runPolyGetFeatureTests(self, provider):
283+
assert len([f for f in provider.getFeatures()]) == 4
284+
285+
# geometry
286+
self.assert_query(provider, 'x($geometry) < -70', [1])
287+
self.assert_query(provider, 'y($geometry) > 79', [1, 2])
288+
self.assert_query(provider, 'xmin($geometry) < -70', [1, 3])
289+
self.assert_query(provider, 'ymin($geometry) < 76', [3])
290+
self.assert_query(provider, 'xmax($geometry) > -68', [2, 3])
291+
self.assert_query(provider, 'ymax($geometry) > 80', [1, 2])
292+
self.assert_query(provider, 'area($geometry) > 10', [1])
293+
self.assert_query(provider, 'perimeter($geometry) < 12', [2, 3])
294+
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])
295+
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])
296+
self.assert_query(provider, 'crosses($geometry,geom_from_wkt( \'Linestring (-68.2 82.1, -66.95 82.1, -66.95 79.05)\'))', [2])
297+
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])
298+
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])
299+
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])
300+
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])
301+
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])
302+
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])
303+
self.assert_query(provider, 'distance($geometry,geom_from_wkt( \'Point (-70 70)\')) > 7', [1, 2])
304+
243305
def testGetFeaturesUncompiled(self):
244306
self.compiled = False
245307
try:
246308
self.disableCompiler()
247309
except AttributeError:
248310
pass
249311
self.runGetFeatureTests(self.provider)
312+
if hasattr(self, 'poly_provider'):
313+
self.runPolyGetFeatureTests(self.poly_provider)
250314

251-
def testGetFeaturesCompiled(self):
315+
def testPolyGetFeaturesCompiled(self):
252316
try:
253317
self.enableCompiler()
254318
self.compiled = True
255319
self.runGetFeatureTests(self.provider)
320+
if hasattr(self, 'poly_provider'):
321+
self.runPolyGetFeatureTests(self.poly_provider)
256322
except AttributeError:
257323
print('Provider does not support compiling')
258324

0 commit comments

Comments
 (0)