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