Skip to content

Commit 0b76352

Browse files
authored
Merge pull request #4642 from nyalldawson/mssql
MSSQL provider fixes
2 parents 6f42b78 + 62af54e commit 0b76352

File tree

8 files changed

+264
-16
lines changed

8 files changed

+264
-16
lines changed

src/providers/mssql/qgsmssqlexpressioncompiler.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,3 +77,12 @@ QString QgsMssqlExpressionCompiler::quotedValue( const QVariant& value, bool& ok
7777
return QgsSqlExpressionCompiler::quotedValue( value, ok );
7878
}
7979
}
80+
81+
QString QgsMssqlExpressionCompiler::quotedIdentifier( const QString &identifier )
82+
{
83+
QString quoted = identifier;
84+
quoted.replace( '[', "[[" );
85+
quoted.replace( ']', "]]" );
86+
quoted = quoted.prepend( '[' ).append( ']' );
87+
return quoted;
88+
}

src/providers/mssql/qgsmssqlexpressioncompiler.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ class QgsMssqlExpressionCompiler : public QgsSqlExpressionCompiler
2929
protected:
3030
virtual Result compileNode( const QgsExpression::Node* node, QString& result ) override;
3131
virtual QString quotedValue( const QVariant& value, bool& ok ) override;
32+
virtual QString quotedIdentifier( const QString& identifier ) override;
3233

3334
};
3435

src/providers/mssql/qgsmssqlfeatureiterator.cpp

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest& request )
109109
mStatement += QString( ",[%1]" ).arg( mSource->mGeometryColName );
110110
}
111111

112-
mStatement += QString( "FROM [%1].[%2]" ).arg( mSource->mSchemaName, mSource->mTableName );
112+
mStatement += QString( " FROM [%1].[%2]" ).arg( mSource->mSchemaName, mSource->mTableName );
113113

114114
bool filterAdded = false;
115115
// set spatial filter
@@ -127,7 +127,7 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest& request )
127127
<< qgsDoubleToString( request.filterRect().xMinimum() ) << ' ' << qgsDoubleToString( request.filterRect().yMaximum() ) << ", "
128128
<< qgsDoubleToString( request.filterRect().xMinimum() ) << ' ' << qgsDoubleToString( request.filterRect().yMinimum() );
129129

130-
mStatement += QString( " where [%1].STIsValid() = 1 AND [%1].STIntersects([%2]::STGeomFromText('POLYGON((%3))',%4)) = 1" ).arg(
130+
mStatement += QString( " where [%1].STIsValid() = 1 AND [%1].Filter([%2]::STGeomFromText('POLYGON((%3))',%4)) = 1" ).arg(
131131
mSource->mGeometryColName, mSource->mGeometryColType, r, QString::number( mSource->mSRId ) );
132132
filterAdded = true;
133133
}
@@ -247,7 +247,7 @@ void QgsMssqlFeatureIterator::BuildStatement( const QgsFeatureRequest& request )
247247
mOrderByCompiled = false;
248248
}
249249

250-
if ( !mOrderByCompiled )
250+
if ( !mOrderByCompiled && !request.orderBy().isEmpty() )
251251
limitAtProvider = false;
252252

253253
if ( request.limit() >= 0 && limitAtProvider )
@@ -301,8 +301,25 @@ bool QgsMssqlFeatureIterator::fetchFeature( QgsFeature& feature )
301301
{
302302
QVariant v = mQuery->value( i );
303303
const QgsField &fld = mSource->mFields.at( mAttributesToFetch.at( i ) );
304-
if ( v.type() != fld.type() )
304+
305+
// special handling for time fields
306+
if ( fld.type() == QVariant::Time && v.type() == QVariant::ByteArray )
307+
{
308+
QList<QByteArray> parts = v.toByteArray().split( '\0' );
309+
if ( parts.count() >= 3 )
310+
{
311+
int hours = QString( parts.at( 0 ) ).at( 0 ).toAscii();
312+
int minutes = QString( parts.at( 1 ) ).at( 0 ).toAscii();
313+
int seconds = QString( parts.at( 2 ) ).at( 0 ).toAscii();
314+
v = QTime( hours, minutes, seconds );
315+
}
316+
else
317+
v = QgsVectorDataProvider::convertValue( fld.type(), v.toString() );
318+
}
319+
else if ( v.type() != fld.type() )
320+
{
305321
v = QgsVectorDataProvider::convertValue( fld.type(), v.toString() );
322+
}
306323
feature.setAttribute( mAttributesToFetch.at( i ), v );
307324
}
308325

@@ -311,12 +328,19 @@ bool QgsMssqlFeatureIterator::fetchFeature( QgsFeature& feature )
311328
if ( mSource->isSpatial() )
312329
{
313330
QByteArray ar = mQuery->record().value( mSource->mGeometryColName ).toByteArray();
314-
unsigned char* wkb = mParser.ParseSqlGeometry(( unsigned char* )ar.data(), ar.size() );
315-
if ( wkb )
331+
if ( !ar.isEmpty() )
316332
{
317-
QgsGeometry *g = new QgsGeometry();
318-
g->fromWkb( wkb, mParser.GetWkbLen() );
319-
feature.setGeometry( g );
333+
unsigned char* wkb = mParser.ParseSqlGeometry(( unsigned char* )ar.data(), ar.size() );
334+
if ( wkb )
335+
{
336+
QgsGeometry *g = new QgsGeometry();
337+
g->fromWkb( wkb, mParser.GetWkbLen() );
338+
feature.setGeometry( g );
339+
}
340+
else
341+
{
342+
feature.setGeometry( nullptr );
343+
}
320344
}
321345
else
322346
{

src/providers/mssql/qgsmssqlprovider.cpp

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,11 @@ void QgsMssqlProvider::loadMetadata()
350350
mSRId = 0;
351351
mWkbType = QGis::WKBUnknown;
352352

353+
if ( !mDatabase.isOpen() )
354+
{
355+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
356+
}
357+
353358
QSqlQuery query = QSqlQuery( mDatabase );
354359
query.setForwardOnly( true );
355360
if ( !query.exec( QString( "select f_geometry_column, coord_dimension, srid, geometry_type from geometry_columns where f_table_schema = '%1' and f_table_name = '%2'" ).arg( mSchemaName, mTableName ) ) )
@@ -368,7 +373,12 @@ void QgsMssqlProvider::loadFields()
368373
{
369374
mAttributeFields.clear();
370375
mDefaultValues.clear();
376+
371377
// get field spec
378+
if ( !mDatabase.isOpen() )
379+
{
380+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
381+
}
372382
QSqlQuery query = QSqlQuery( mDatabase );
373383
query.setForwardOnly( true );
374384
if ( !query.exec( QString( "exec sp_columns @table_name = N'%1', @table_owner = '%2'" ).arg( mTableName, mSchemaName ) ) )
@@ -532,6 +542,10 @@ QVariant QgsMssqlProvider::minimumValue( int index )
532542
sql += QString( " where (%1)" ).arg( mSqlWhereClause );
533543
}
534544

545+
if ( !mDatabase.isOpen() )
546+
{
547+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
548+
}
535549
QSqlQuery query = QSqlQuery( mDatabase );
536550
query.setForwardOnly( true );
537551

@@ -563,6 +577,10 @@ QVariant QgsMssqlProvider::maximumValue( int index )
563577
sql += QString( " where (%1)" ).arg( mSqlWhereClause );
564578
}
565579

580+
if ( !mDatabase.isOpen() )
581+
{
582+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
583+
}
566584
QSqlQuery query = QSqlQuery( mDatabase );
567585
query.setForwardOnly( true );
568586

@@ -603,6 +621,10 @@ void QgsMssqlProvider::uniqueValues( int index, QList<QVariant> &uniqueValues, i
603621
sql += QString( " where (%1)" ).arg( mSqlWhereClause );
604622
}
605623

624+
if ( !mDatabase.isOpen() )
625+
{
626+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
627+
}
606628
QSqlQuery query = QSqlQuery( mDatabase );
607629
query.setForwardOnly( true );
608630

@@ -631,6 +653,10 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate )
631653
// get features to calculate the statistics
632654
QString statement;
633655

656+
if ( !mDatabase.isOpen() )
657+
{
658+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
659+
}
634660
QSqlQuery query = QSqlQuery( mDatabase );
635661
query.setForwardOnly( true );
636662

@@ -664,14 +690,14 @@ void QgsMssqlProvider::UpdateStatistics( bool estimate )
664690
if ( estimate )
665691
{
666692
if ( mGeometryColType == "geometry" )
667-
statement = QString( "select min([%1].MakeValid().STPointN(1).STX), min([%1].MakeValid().STPointN(1).STY), max([%1].MakeValid().STPointN(1).STX), max([%1].MakeValid().STPointN(1).STY)" ).arg( mGeometryColName );
693+
statement = QString( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STX else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STY else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STX else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).STY else NULL end)" ).arg( mGeometryColName );
668694
else
669-
statement = QString( "select min([%1].MakeValid().STPointN(1).Long), min([%1].MakeValid().STPointN(1).Lat), max([%1].MakeValid().STPointN(1).Long), max([%1].MakeValid().STPointN(1).Lat)" ).arg( mGeometryColName );
695+
statement = QString( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Long else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Lat else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Long else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STPointN(1).Lat else NULL end)" ).arg( mGeometryColName );
670696
}
671697
else
672698
{
673699
if ( mGeometryColType == "geometry" )
674-
statement = QString( "select min([%1].MakeValid().STEnvelope().STPointN(1).STX), min([%1].MakeValid().STEnvelope().STPointN(1).STY), max([%1].MakeValid().STEnvelope().STPointN(3).STX), max([%1].MakeValid().STEnvelope().STPointN(3).STY)" ).arg( mGeometryColName );
700+
statement = QString( "select min(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(1).STX else NULL end), min(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(1).STY else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(3).STX else NULL end), max(case when ([%1].STIsValid() = 1) THEN [%1].STEnvelope().STPointN(3).STY else NULL end)" ).arg( mGeometryColName );
675701
else
676702
{
677703
statement = QString( "select [%1]" ).arg( mGeometryColName );
@@ -758,6 +784,10 @@ long QgsMssqlProvider::featureCount() const
758784

759785
// If there is no subset set we can get the count from the system tables.
760786
// Which is faster then doing select count(*)
787+
if ( !mDatabase.isOpen() )
788+
{
789+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
790+
}
761791
QSqlQuery query = QSqlQuery( mDatabase );
762792
query.setForwardOnly( true );
763793

@@ -1419,6 +1449,10 @@ QgsCoordinateReferenceSystem QgsMssqlProvider::crs()
14191449
return mCrs;
14201450

14211451
// try to load crs from the database tables as a fallback
1452+
if ( !mDatabase.isOpen() )
1453+
{
1454+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
1455+
}
14221456
QSqlQuery query = QSqlQuery( mDatabase );
14231457
query.setForwardOnly( true );
14241458
bool execOk = query.exec( QString( "select srtext from spatial_ref_sys where srid = %1" ).arg( QString::number( mSRId ) ) );
@@ -1472,6 +1506,10 @@ bool QgsMssqlProvider::setSubsetString( const QString& theSQL, bool )
14721506
sql += QString( " where %1" ).arg( mSqlWhereClause );
14731507
}
14741508

1509+
if ( !mDatabase.isOpen() )
1510+
{
1511+
mDatabase = GetDatabase( mService, mHost, mDatabaseName, mUserName, mPassword );
1512+
}
14751513
QSqlQuery query = QSqlQuery( mDatabase );
14761514
query.setForwardOnly( true );
14771515
if ( !query.exec( sql ) )

src/providers/mssql/qgsmssqlprovider.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ class QgsMssqlProvider : public QgsVectorDataProvider
290290
QGis::WkbType mWkbType;
291291

292292
// The database object
293-
QSqlDatabase mDatabase;
293+
mutable QSqlDatabase mDatabase;
294294

295295
// The current sql query
296296
QSqlQuery mQuery;

src/providers/mssql/qgsmssqlsourceselect.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ void QgsMssqlSourceSelect::on_btnConnect_clicked()
495495

496496
bool allowGeometrylessTables = cbxAllowGeometrylessTables->isChecked();
497497

498-
bool estimateMetadata = settings.value( key + "/estimatedMetadata", true ).toBool();
498+
mUseEstimatedMetadata = settings.value( key + "/estimatedMetadata", true ).toBool();
499499

500500
mConnInfo = "dbname='" + database + '\'';
501501
if ( !host.isEmpty() )
@@ -585,7 +585,7 @@ void QgsMssqlSourceSelect::on_btnConnect_clicked()
585585
{
586586
if ( type == "GEOMETRY" || type.isNull() || srid.isEmpty() )
587587
{
588-
addSearchGeometryColumn( connectionName, layer, estimateMetadata );
588+
addSearchGeometryColumn( connectionName, layer, mUseEstimatedMetadata );
589589
type = "";
590590
srid = "";
591591
}

tests/src/python/test_provider_mssql.py

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
import os
1818

19-
from qgis.core import QgsVectorLayer, QgsFeatureRequest
19+
from qgis.core import QgsVectorLayer, QgsFeatureRequest, QgsRectangle
2020

2121
from qgis.PyQt.QtCore import QSettings, QDate, QTime, QDateTime, QVariant
2222

@@ -56,6 +56,50 @@ def enableCompiler(self):
5656
def disableCompiler(self):
5757
QSettings().setValue(u'/qgis/compileExpressions', False)
5858

59+
def uncompiledFilters(self):
60+
filters = set(['"name" NOT LIKE \'Ap%\'',
61+
'"name" IS NULL',
62+
'"name" IS NOT NULL',
63+
'"name" NOT ILIKE \'QGIS\'',
64+
'"name" NOT ILIKE \'pEAR\'',
65+
'name <> \'Apple\'',
66+
'"name" <> \'apple\'',
67+
'(name = \'Apple\') is not null',
68+
'"name" || \' \' || "cnt" = \'Orange 100\'',
69+
'\'x\' || "name" IS NOT NULL',
70+
'\'x\' || "name" IS NULL',
71+
'"name" ~ \'[OP]ra[gne]+\'',
72+
'false and NULL',
73+
'true and NULL',
74+
'NULL and false',
75+
'NULL and true',
76+
'NULL and NULL',
77+
'false or NULL',
78+
'true or NULL',
79+
'NULL or false',
80+
'NULL or true',
81+
'NULL or NULL',
82+
'not null',
83+
'not name IS NULL',
84+
'not name = \'Apple\'',
85+
'not name = \'Apple\' or name = \'Apple\'',
86+
'not name = \'Apple\' or not name = \'Apple\'',
87+
'not name = \'Apple\' and pk = 4',
88+
'not name = \'Apple\' and not pk = 4',
89+
'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))\'))'])
90+
return filters
91+
92+
def partiallyCompiledFilters(self):
93+
return set(['name ILIKE \'QGIS\'',
94+
'name = \'Apple\'',
95+
'name = \'apple\'',
96+
'name LIKE \'Apple\'',
97+
'name LIKE \'aPple\'',
98+
'"name"="name2"',
99+
'name ILIKE \'aPple\'',
100+
'name ILIKE \'%pp%\'',
101+
'"name" || \' \' || "name" = \'Orange Orange\''])
102+
59103
# HERE GO THE PROVIDER SPECIFIC TESTS
60104
def testDateTimeTypes(self):
61105
vl = QgsVectorLayer('%s table="qgis_test"."date_times" sql=' %
@@ -83,5 +127,34 @@ def testDateTimeTypes(self):
83127
self.assertEqual(f.attributes()[datetime_idx], QDateTime(
84128
QDate(2004, 3, 4), QTime(13, 41, 52)))
85129

130+
def testInvalidGeometries(self):
131+
""" Test what happens when SQL Server is a POS and throws an exception on encountering an invalid geometry """
132+
vl = QgsVectorLayer('%s srid=4167 type=POLYGON table="qgis_test"."invalid_polys" (ogr_geometry) sql=' %
133+
(self.dbconn), "testinvalid", "mssql")
134+
assert(vl.isValid())
135+
136+
self.assertEqual(vl.dataProvider().extent().toString(1), QgsRectangle(173.953, -41.513, 173.967, -41.502).toString(1))
137+
138+
#burn through features - don't want SQL server to trip up on the invalid ones
139+
count = 0
140+
for f in vl.dataProvider().getFeatures():
141+
count += 1
142+
self.assertEqual(count, 39)
143+
144+
count = 0
145+
146+
for f in vl.dataProvider().getFeatures(QgsFeatureRequest(QgsRectangle(173, -42, 174, -41))):
147+
count += 1
148+
# two invalid geometry features
149+
self.assertEqual(count, 37)
150+
# sorry... you get NO chance to see these features exist and repair them... because SQL server. Use PostGIS instead and live a happier life!
151+
152+
# with estimated metadata
153+
vl = QgsVectorLayer('%s srid=4167 type=POLYGON estimatedmetadata=true table="qgis_test"."invalid_polys" (ogr_geometry) sql=' %
154+
(self.dbconn), "testinvalid", "mssql")
155+
assert(vl.isValid())
156+
self.assertEqual(vl.dataProvider().extent().toString(1), QgsRectangle(173.954, -41.513, 173.967, -41.502).toString(1))
157+
158+
86159
if __name__ == '__main__':
87160
unittest.main()

0 commit comments

Comments
 (0)