1 /***************************************************************************
2   qgsdb2featureiterator.cpp - DB2 spatial feature processing
3   --------------------------------------
4   Date      : 2016-01-27
5   Copyright : (C) 2016 by David Adler
6                           Shirley Xiao, David Nguyen
7   Email     : dadler at adtechgeospatial.com
8               xshirley2012 at yahoo.com,  davidng0123 at gmail.com
9   Adapted from MSSQL provider by Tamas Szekeres
10 ****************************************************************************
11  *
12  * This program is free software; you can redistribute it and/or modify
13  * it under the terms of the GNU General Public License as published by
14  * the Free Software Foundation; either version 2 of the License, or
15  * (at your option) any later version.
16  *
17  ***************************************************************************/
18 
19 #include "qgsdb2featureiterator.h"
20 #include "qgsdb2provider.h"
21 #include "qgsdb2expressioncompiler.h"
22 #include "qgssettings.h"
23 #include "qgslogger.h"
24 #include "qgsgeometry.h"
25 #include "qgsexception.h"
26 
27 #include <QObject>
28 #include <QTextStream>
29 #include <QSqlRecord>
30 
31 
QgsDb2FeatureIterator(QgsDb2FeatureSource * source,bool ownSource,const QgsFeatureRequest & request)32 QgsDb2FeatureIterator::QgsDb2FeatureIterator( QgsDb2FeatureSource *source, bool ownSource, const QgsFeatureRequest &request )
33   : QgsAbstractFeatureIteratorFromSource<QgsDb2FeatureSource>( source, ownSource, request )
34 {
35   mClosed = false;
36 
37   if ( mRequest.destinationCrs().isValid() && mRequest.destinationCrs() != mSource->mCrs )
38   {
39     mTransform = QgsCoordinateTransform( mSource->mCrs, mRequest.destinationCrs(), mRequest.transformContext() );
40   }
41   try
42   {
43     mFilterRect = filterRectToSourceCrs( mTransform );
44   }
45   catch ( QgsCsException & )
46   {
47     // can't reproject mFilterRect
48     close();
49     return;
50   }
51 
52   BuildStatement( request );
53 
54   // WARNING - we can't obtain the database connection now, as this method should be
55   // run from the main thread, yet iteration can be done in a different thread.
56   // This would result in failure, because QSqlDatabase instances cannot be used
57   // from a different thread where they were created. Instead, we defer creation
58   // of the database until the first feature is fetched.
59 }
60 
61 
~QgsDb2FeatureIterator()62 QgsDb2FeatureIterator::~QgsDb2FeatureIterator()
63 {
64   QgsDebugMsg( QStringLiteral( "Fetch count at close: %1" ).arg( mFetchCount ) );
65   close();
66 }
67 
BuildStatement(const QgsFeatureRequest & request)68 void QgsDb2FeatureIterator::BuildStatement( const QgsFeatureRequest &request )
69 {
70   bool limitAtProvider = ( mRequest.limit() >= 0 );
71   QString delim;
72 
73   // build sql statement
74   mStatement = QStringLiteral( "SELECT " );
75 
76   if ( !mSource->mFidColName.isEmpty() )
77   {
78     mStatement += mSource->mFidColName;
79     mFidCol = mSource->mFields.indexFromName( mSource->mFidColName );
80     mAttributesToFetch.append( mFidCol );
81     delim = QStringLiteral( "," );
82   }
83 
84   bool subsetOfAttributes = mRequest.flags() & QgsFeatureRequest::SubsetOfAttributes;
85   QgsAttributeList attrs = subsetOfAttributes ? mRequest.subsetOfAttributes() : mSource->mFields.allAttributesList();
86 
87   // ensure that all attributes required for expression filter are being fetched
88   if ( subsetOfAttributes && request.filterType() == QgsFeatureRequest::FilterExpression )
89   {
90     //ensure that all fields required for filter expressions are prepared
91     QSet<int> attributeIndexes = request.filterExpression()->referencedAttributeIndexes( mSource->mFields );
92     attributeIndexes += qgis::listToSet( attrs );
93     attrs = qgis::setToList( attributeIndexes );
94   }
95 
96   const auto constAttrs = attrs;
97   for ( int i : constAttrs )
98   {
99     QString fieldname = mSource->mFields.at( i ).name();
100     if ( mSource->mFidColName == fieldname )
101       continue;
102     mStatement += delim + fieldname;
103     delim = QStringLiteral( "," );
104     mAttributesToFetch.append( i );
105     QgsDebugMsg( QStringLiteral( "i: %1; name: %2" ).arg( i ).arg( fieldname ) );
106   }
107 
108   // get geometry col if requested and table has spatial column
109   if ( (
110          !( request.flags() & QgsFeatureRequest::NoGeometry )
111          || ( request.filterType() == QgsFeatureRequest::FilterExpression && request.filterExpression()->needsGeometry() )
112        )
113        && mSource->isSpatial() )
114   {
115     mStatement += QString( delim + "DB2GSE.ST_ASBINARY(%1) AS %1 " ).arg( mSource->mGeometryColName );
116     mAttributesToFetch.append( 2 );  // dummy - won't store geometry as an attribute
117   }
118 
119   mStatement += QStringLiteral( " FROM %1.%2" ).arg( mSource->mSchemaName, mSource->mTableName );
120 
121   bool filterAdded = false;
122   // set spatial filter
123   if ( !mFilterRect.isNull() && mSource->isSpatial() && !mFilterRect.isEmpty() )
124   {
125     if ( mRequest.flags() & QgsFeatureRequest::ExactIntersect )
126     {
127       QString rectangleWkt = mFilterRect.asWktPolygon();
128       QgsDebugMsg( "filter polygon: " + rectangleWkt );
129       mStatement += QStringLiteral( " WHERE DB2GSE.ST_Intersects(%1, DB2GSE.ST_POLYGON('%2', %3)) = 1" ).arg(
130                       mSource->mGeometryColName,
131                       rectangleWkt,
132                       QString::number( mSource->mSRId ) );
133     }
134     else
135     {
136       mStatement += QStringLiteral( " WHERE DB2GSE.ENVELOPESINTERSECT(%1, %2, %3, %4, %5, %6) = 1" ).arg(
137                       mSource->mGeometryColName,
138                       qgsDoubleToString( mFilterRect.xMinimum() ),
139                       qgsDoubleToString( mFilterRect.yMinimum() ),
140                       qgsDoubleToString( mFilterRect.xMaximum() ),
141                       qgsDoubleToString( mFilterRect.yMaximum() ),
142                       QString::number( mSource->mSRId ) );
143     }
144     filterAdded = true;
145   }
146 
147   // set fid filter
148   if ( request.filterType() == QgsFeatureRequest::FilterFid && !mSource->mFidColName.isEmpty() )
149   {
150     QString fidfilter = QStringLiteral( " %1 = %2" ).arg( mSource->mFidColName, QString::number( request.filterFid() ) );
151     // set attribute filter
152     if ( !filterAdded )
153       mStatement += QLatin1String( " WHERE " );
154     else
155       mStatement += QLatin1String( " AND " );
156 
157     mStatement += fidfilter;
158     filterAdded = true;
159   }
160   else if ( request.filterType() == QgsFeatureRequest::FilterFids && !mSource->mFidColName.isEmpty()
161             && !mRequest.filterFids().isEmpty() )
162   {
163     QString delim;
164     QString inClause = QStringLiteral( "%1 IN (" ).arg( mSource->mFidColName );
165     const auto constFilterFids = mRequest.filterFids();
166     for ( QgsFeatureId featureId : constFilterFids )
167     {
168       inClause += delim + FID_TO_STRING( featureId );
169       delim = ',';
170     }
171     inClause.append( ')' );
172 
173     if ( !filterAdded )
174       mStatement += QLatin1String( " WHERE " );
175     else
176       mStatement += QLatin1String( " AND " );
177 
178     mStatement += inClause;
179     filterAdded = true;
180   }
181 
182   if ( !mSource->mSqlWhereClause.isEmpty() )
183   {
184     if ( !filterAdded )
185       mStatement += " WHERE (" + mSource->mSqlWhereClause + ")";
186     else
187       mStatement += " AND (" + mSource->mSqlWhereClause + ")";
188   }
189 
190   mExpressionCompiled = false;
191   mCompileStatus = NoCompilation;
192   if ( request.filterType() == QgsFeatureRequest::FilterExpression )
193   {
194     QgsDebugMsg( QStringLiteral( "compileExpressions: %1" ).arg( QgsSettings().value( "qgis/compileExpressions", true ).toString() ) );
195     if ( QgsSettings().value( QStringLiteral( "qgis/compileExpressions" ), true ).toBool() )
196     {
197       QgsDb2ExpressionCompiler compiler = QgsDb2ExpressionCompiler( mSource );
198       QgsDebugMsg( "expression dump: " + request.filterExpression()->dump() );
199       QgsDebugMsg( "expression expression: " + request.filterExpression()->expression() );
200       QgsSqlExpressionCompiler::Result result = compiler.compile( request.filterExpression() );
201       QgsDebugMsg( QStringLiteral( "compiler result: %1" ).arg( result ) + "; query: " + compiler.result() );
202       if ( result == QgsSqlExpressionCompiler::Complete || result == QgsSqlExpressionCompiler::Partial )
203       {
204         if ( !filterAdded )
205           mStatement += " WHERE (" + compiler.result() + ')';
206         else
207           mStatement += " AND (" + compiler.result() + ')';
208 
209         //if only partial success when compiling expression, we need to double-check results using QGIS' expressions
210         mExpressionCompiled = ( result == QgsSqlExpressionCompiler::Complete );
211         mCompileStatus = ( mExpressionCompiled ? Compiled : PartiallyCompiled );
212         limitAtProvider = mExpressionCompiled;
213       }
214       else
215       {
216         limitAtProvider = false;
217       }
218     }
219     else
220     {
221       limitAtProvider = false;
222     }
223   }
224 
225   QStringList orderByParts;
226   mOrderByCompiled = true;
227   QgsDebugMsg( QStringLiteral( "compileExpressions: %1" ).arg( QgsSettings().value( "qgis/compileExpressions", true ).toString() ) );
228   if ( QgsSettings().value( QStringLiteral( "qgis/compileExpressions" ), true ).toBool() && limitAtProvider )
229   {
230     const auto constOrderBy = request.orderBy();
231     for ( const QgsFeatureRequest::OrderByClause &clause : constOrderBy )
232     {
233       QgsDebugMsg( QStringLiteral( "processing a clause; ascending: %1; nullsFirst: %2" ).arg( clause.ascending() ).arg( clause.nullsFirst() ) );
234 
235       if ( ( clause.ascending() && clause.nullsFirst() ) || ( !clause.ascending() && !clause.nullsFirst() ) )
236       {
237         // Not supported by DB2
238         // NULLs are last in ascending order
239         mOrderByCompiled = false;
240         QgsDebugMsg( QStringLiteral( "ascending with nullsFirst not supported" ) );
241         break;
242       }
243 
244       QgsDb2ExpressionCompiler compiler = QgsDb2ExpressionCompiler( mSource );
245       QgsExpression expression = clause.expression();
246       QgsDebugMsg( "expression: " + expression.dump() );
247       if ( compiler.compile( &expression ) == QgsSqlExpressionCompiler::Complete )
248       {
249         QgsDebugMsg( QStringLiteral( "compile complete" ) );
250         QString part;
251         part = compiler.result();
252         part += clause.ascending() ? " ASC" : " DESC";
253         orderByParts << part;
254       }
255       else
256       {
257         QgsDebugMsg( "compile of '" + expression.dump() + "' failed" );
258         // Most important clauses at the beginning of the list
259         // will still be sent and used to pre-sort so the local
260         // CPU can use its cycles for fine-tuning.
261         mOrderByCompiled = false;
262       }
263     }
264   }
265   else
266   {
267     mOrderByCompiled = false;
268   }
269 
270   if ( !orderByParts.isEmpty() )
271   {
272     mOrderByClause = QStringLiteral( " ORDER BY %1" ).arg( orderByParts.join( QLatin1Char( ',' ) ) );
273     mStatement += mOrderByClause;
274   }
275 
276   if ( limitAtProvider && request.limit() > 0 )
277   {
278     mStatement += QStringLiteral( " FETCH FIRST %1 ROWS ONLY" ).arg( mRequest.limit() );
279   }
280 
281   QgsDebugMsg( mStatement );
282 }
283 
prepareOrderBy(const QList<QgsFeatureRequest::OrderByClause> & orderBys)284 bool QgsDb2FeatureIterator::prepareOrderBy( const QList<QgsFeatureRequest::OrderByClause> &orderBys )
285 {
286   Q_UNUSED( orderBys )
287   QgsDebugMsg( QStringLiteral( "mOrderByCompiled: %1" ).arg( mOrderByCompiled ) );
288   // Preparation has already been done in the constructor, so we just communicate the result
289   return mOrderByCompiled;
290 }
291 
nextFeatureFilterExpression(QgsFeature & f)292 bool QgsDb2FeatureIterator::nextFeatureFilterExpression( QgsFeature &f )
293 {
294   QgsDebugMsg( QStringLiteral( "mExpressionCompiled: %1" ).arg( mExpressionCompiled ) );
295   if ( !mExpressionCompiled )
296     return QgsAbstractFeatureIterator::nextFeatureFilterExpression( f );
297   else
298     return fetchFeature( f );
299 }
300 
fetchFeature(QgsFeature & feature)301 bool QgsDb2FeatureIterator::fetchFeature( QgsFeature &feature )
302 {
303   feature.setValid( false );
304 
305   if ( !mDatabase.isValid() )
306   {
307     // No existing connection, so set it up now. It's safe to do here as we're now in
308     // the thread were iteration is actually occurring.
309     // connect to the database
310     QString errMsg;
311     QgsDebugMsg( QStringLiteral( "fetchFeature getDatabase" ) );
312     mDatabase = QgsDb2Provider::getDatabase( mSource->mConnInfo, errMsg );
313     QgsDebugMsg( QStringLiteral( "fetchFeature back from getDatabase" ) );
314     if ( !errMsg.isEmpty() )
315     {
316       QgsDebugMsg( "Failed to open database: " + errMsg );
317       return false;
318     }
319 
320     // create sql query
321     mQuery.reset( new QSqlQuery( mDatabase ) );
322 
323     // start selection
324     if ( !rewind() )
325       return false;
326   }
327 
328   if ( !mQuery )
329   {
330     QgsDebugMsg( QStringLiteral( "Read attempt on no query" ) );
331     return false;
332   }
333 
334   if ( !mQuery->isActive() )
335   {
336     QgsDebugMsg( QStringLiteral( "Read attempt on inactive query" ) );
337     return false;
338   }
339 
340   if ( mQuery->next() )
341   {
342     feature.initAttributes( mSource->mFields.count() );
343     feature.setFields( mSource->mFields ); // allow name-based attribute lookups
344     QSqlRecord record = mQuery->record();
345     for ( int i = 0; i < mAttributesToFetch.count(); i++ )
346     {
347       QVariant v = mQuery->value( i );
348       QString attrName = record.fieldName( i );
349       if ( attrName == mSource->mGeometryColName )
350       {
351 //        QgsDebugMsg( QStringLiteral( "Geom col: %1" ).arg( attrName ) ); // not sure why we set geometry as a field value
352       }
353       else
354       {
355 //        QgsDebugMsg( QStringLiteral( "Field: %1; value: %2" ).arg( attrName, v.toString() ) );
356 
357         /**
358          * CHAR and VARCHAR fields seem to get corrupted sometimes when directly
359          * calling feature.setAttribute(..., v) with mQuery->value(i). Workaround
360          * that seems to fix the problem is to call v = QVariant(v.toString()).
361          */
362         if ( v.type() == QVariant::String )
363         {
364           v = QVariant( v.toString() );
365         }
366         QgsField fld = mSource->mFields.at( mAttributesToFetch.at( i ) );
367 //        QgsDebugMsg( QStringLiteral( "v.type: %1; fld.type: %2" ).arg( v.type() ).arg( fld.type() ) );
368         if ( v.type() != fld.type() )
369         {
370           v = QgsVectorDataProvider::convertValue( fld.type(), v.toString() );
371         }
372         feature.setAttribute( mAttributesToFetch[i], v );
373       }
374     }
375 //    QgsDebugMsg( QStringLiteral( "Fid: %1; value: %2" ).arg( mSource->mFidColName ).arg( record.value( mSource->mFidColName ).toLongLong() ) );
376     feature.setId( mQuery->record().value( mSource->mFidColName ).toLongLong() );
377 
378     if ( mSource->isSpatial() )
379     {
380       QByteArray ar = record.value( mSource->mGeometryColName ).toByteArray();
381       int wkb_size = ar.size();
382       if ( 0 < wkb_size )
383       {
384         unsigned char *db2data = new unsigned char[wkb_size + 1]; // allocate persistent storage
385         memcpy( db2data, ( unsigned char * )ar.data(), wkb_size + 1 );
386         QgsGeometry g;
387         g.fromWkb( db2data, wkb_size );
388         feature.setGeometry( g );
389       }
390       else
391       {
392         QgsDebugMsg( QStringLiteral( "Geometry is empty" ) );
393         feature.clearGeometry();
394       }
395     }
396     else
397     {
398       feature.clearGeometry();
399     }
400     feature.setValid( true );
401     mFetchCount++;
402     geometryToDestinationCrs( feature, mTransform );
403     if ( mFetchCount % 100 == 0 )
404     {
405       QgsDebugMsg( QStringLiteral( "Fetch count: %1" ).arg( mFetchCount ) );
406     }
407     return true;
408   }
409   QgsDebugMsg( QStringLiteral( "No feature; lastError: '%1'" ).arg( mQuery->lastError().text() ) );
410   return false;
411 }
412 
rewind()413 bool QgsDb2FeatureIterator::rewind()
414 {
415   if ( mClosed )
416   {
417     QgsDebugMsg( QStringLiteral( "iterator closed" ) );
418     return false;
419   }
420 
421   if ( mStatement.isEmpty() )
422   {
423     QgsDebugMsg( QStringLiteral( "rewind on empty statement" ) );
424     return false;
425   }
426 
427   if ( !mQuery )
428     return false;
429 
430   mQuery->clear();
431   mQuery->setForwardOnly( true );
432   QgsDebugMsg( "Execute mStatement: " + mStatement );
433   if ( !mQuery->exec( mStatement ) )
434   {
435     QgsDebugMsg( mQuery->lastError().text() );
436     close();
437     return false;
438   }
439   QgsDebugMsg( QStringLiteral( "leaving rewind" ) );
440   QgsDebugMsg( mQuery->lastError().text() );
441   mFetchCount = 0;
442   return true;
443 }
444 
close()445 bool QgsDb2FeatureIterator::close()
446 {
447   if ( mClosed )
448     return false;
449 
450   if ( mQuery )
451   {
452     if ( !mQuery->isActive() )
453     {
454       QgsDebugMsg( QStringLiteral( "QgsDb2FeatureIterator::close on inactive query" ) );
455     }
456     else
457     {
458       mQuery->finish();
459     }
460     mQuery.reset();
461   }
462 
463   if ( mDatabase.isOpen() )
464   {
465     mDatabase.close();
466   }
467 
468   iteratorClosed();
469 
470   mClosed = true;
471   return true;
472 }
473 
474 ///////////////
475 
QgsDb2FeatureSource(const QgsDb2Provider * p)476 QgsDb2FeatureSource::QgsDb2FeatureSource( const QgsDb2Provider *p )
477   : mFields( p->mAttributeFields )
478   , mFidColName( p->mFidColName )
479   , mSRId( p->mSRId )
480   , mGeometryColName( p->mGeometryColName )
481   , mGeometryColType( p->mGeometryColType )
482   , mSchemaName( p->mSchemaName )
483   , mTableName( p->mTableName )
484   , mConnInfo( p->mConnInfo )
485   , mSqlWhereClause( p->mSqlWhereClause )
486   , mCrs( p->crs() )
487 {}
488 
getFeatures(const QgsFeatureRequest & request)489 QgsFeatureIterator QgsDb2FeatureSource::getFeatures( const QgsFeatureRequest &request )
490 {
491   return QgsFeatureIterator( new QgsDb2FeatureIterator( this, false, request ) );
492 }
493