Statistics
| Branch: | Tag: | Revision:

qgis / src / providers / mssql / qgsmssqlprovider.cpp @ master

History | View | Annotate | Download (110 KB)

1
/***************************************************************************
2
      qgsmssqlprovider.cpp  -  Data provider for mssql server
3
                             -------------------
4
    begin                : 2011-10-08
5
    copyright            : (C) 2011 by Tamas Szekeres
6
    email                : szekerest at gmail.com
7
 ***************************************************************************/
8

    
9
/***************************************************************************
10
 *                                                                         *
11
 *   This program is free software; you can redistribute it and/or modify  *
12
 *   it under the terms of the GNU General Public License as published by  *
13
 *   the Free Software Foundation; either version 2 of the License, or     *
14
 *   (at your option) any later version.                                   *
15
 *                                                                         *
16
 ***************************************************************************/
17

    
18
#include "qgsmssqlprovider.h"
19
#include "moc_qgsmssqlprovider.cpp"
20
#include "qgsmssqlconnection.h"
21
#include "qgsmssqldatabase.h"
22
#include "qgsmssqlproviderconnection.h"
23
#include "qgsfeedback.h"
24
#include "qgsdbquerylog.h"
25
#include "qgsdbquerylog_p.h"
26
#include "qgsvariantutils.h"
27

    
28
#include <QtGlobal>
29
#include <QFileInfo>
30
#include <QDataStream>
31
#include <QStringList>
32
#include <QSettings>
33
#include <QRegularExpression>
34
#include <QUrl>
35
#include <QtSql/QSqlDatabase>
36
#include <QtSql/QSqlQuery>
37
#include <QtSql/QSqlError>
38
#include <QtSql/QSqlRecord>
39
#include <QtSql/QSqlField>
40
#include <QStringBuilder>
41
#include <QWaitCondition>
42

    
43

    
44
#include "qgsapplication.h"
45
#include "qgsdataprovider.h"
46
#include "qgsfeature.h"
47
#include "qgsfields.h"
48
#include "qgsgeometry.h"
49
#include "qgslogger.h"
50
#include "qgsmessagelog.h"
51
#include "qgsrectangle.h"
52
#include "qgis.h"
53

    
54
#include "qgsmssqldataitems.h"
55
#include "qgsmssqlfeatureiterator.h"
56
#include "qgsmssqltransaction.h"
57

    
58

    
59
#include "qgsconfig.h"
60
constexpr int sMssqlConQueryLogFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR );
61
#define LoggedExec( query, sql ) execLogged( query, sql, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
62
#define LoggedExecPrepared( query ) execPreparedLogged( query, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
63
#define LoggedExecMetadata( query, sql, uri ) execLogged( query, sql, uri, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
64

    
65

    
66
const QString QgsMssqlProvider::MSSQL_PROVIDER_KEY = QStringLiteral( "mssql" );
67
const QString QgsMssqlProvider::MSSQL_PROVIDER_DESCRIPTION = QStringLiteral( "MSSQL spatial data provider" );
68
int QgsMssqlProvider::sConnectionId = 0;
69

    
70
QgsMssqlProvider::QgsMssqlProvider( const QString &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags )
71
  : QgsVectorDataProvider( uri, options, flags )
72
  , mShared( new QgsMssqlSharedData )
73
{
74
  const QgsDataSourceUri anUri = QgsDataSourceUri( uri );
75

    
76
  if ( !anUri.srid().isEmpty() )
77
    mSRId = anUri.srid().toInt();
78
  else
79
    mSRId = -1;
80

    
81
  mWkbType = anUri.wkbType();
82

    
83
  mValid = true;
84

    
85
  mUserName = anUri.username();
86
  mPassword = anUri.password();
87
  mService = anUri.service();
88
  mDatabaseName = anUri.database();
89
  mHost = anUri.host();
90

    
91
  mUseEstimatedMetadata = anUri.useEstimatedMetadata();
92
  if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
93
  {
94
    mUseEstimatedMetadata = true;
95
  }
96

    
97
  mDisableInvalidGeometryHandling = anUri.hasParam( QStringLiteral( "disableInvalidGeometryHandling" ) )
98
                                      ? anUri.param( QStringLiteral( "disableInvalidGeometryHandling" ) ).toInt()
99
                                      : false;
100

    
101
  mUseGeometryColumnsTableForExtent = anUri.hasParam( QStringLiteral( "extentInGeometryColumns" ) )
102
                                        ? anUri.param( QStringLiteral( "extentInGeometryColumns" ) ).toInt()
103
                                        : false;
104

    
105
  mSqlWhereClause = anUri.sql();
106

    
107
  mConn = QgsMssqlDatabase::connectDb( mService, mHost, mDatabaseName, mUserName, mPassword, false );
108
  if ( !mConn )
109
  {
110
    mValid = false;
111
    return;
112
  }
113
  QSqlDatabase db = mConn->db();
114

    
115
  if ( !db.isOpen() )
116
  {
117
    setLastError( db.lastError().text() );
118
    QgsDebugError( mLastError );
119
    mValid = false;
120
    return;
121
  }
122

    
123
  // Database successfully opened; we can now issue SQL commands.
124
  if ( !anUri.schema().isEmpty() )
125
    mSchemaName = anUri.schema();
126
  else
127
    mSchemaName = QStringLiteral( "dbo" );
128

    
129
  if ( !anUri.table().isEmpty() )
130
  {
131
    // the layer name has been specified
132
    mTableName = anUri.table();
133
    QStringList sl = mTableName.split( '.' );
134
    if ( sl.length() == 2 )
135
    {
136
      mSchemaName = sl[0];
137
      mTableName = sl[1];
138
    }
139
    mTables = QStringList( mTableName );
140
  }
141
  else
142
  {
143
    // Get a list of table
144
    mTables = db.tables( QSql::Tables );
145
    if ( !mTables.isEmpty() )
146
      mTableName = mTables[0];
147
    else
148
      mValid = false;
149
  }
150

    
151
  if ( mValid )
152
  {
153
    if ( !anUri.geometryColumn().isEmpty() )
154
      mGeometryColName = anUri.geometryColumn();
155

    
156
    if ( mSRId < 0 || mWkbType == Qgis::WkbType::Unknown || mGeometryColName.isEmpty() )
157
    {
158
      loadMetadata();
159
    }
160
    loadFields();
161

    
162
    UpdateStatistics( mUseEstimatedMetadata );
163

    
164
    //only for views, defined in layer data when loading layer for first time
165
    bool primaryKeyFromGeometryColumnsTable = anUri.hasParam( QStringLiteral( "primaryKeyInGeometryColumns" ) )
166
                                                ? anUri.param( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toInt()
167
                                                : false;
168

    
169
    QStringList cols;
170
    if ( primaryKeyFromGeometryColumnsTable )
171
    {
172
      mPrimaryKeyType = PktUnknown;
173
      mPrimaryKeyAttrs.clear();
174
      primaryKeyFromGeometryColumnsTable = getPrimaryKeyFromGeometryColumns( cols );
175
      if ( !primaryKeyFromGeometryColumnsTable )
176
        QgsMessageLog::logMessage( tr( "Invalid primary key from geometry_columns table for layer '%1', get primary key from the layer." ).arg( anUri.table() ), tr( "MS SQL Server" ) );
177
    }
178

    
179
    if ( !primaryKeyFromGeometryColumnsTable )
180
    {
181
      const QString primaryKey = anUri.keyColumn();
182
      if ( !primaryKey.isEmpty() )
183
      {
184
        mPrimaryKeyAttrs.clear();
185
        cols = parseUriKey( primaryKey );
186
      }
187
    }
188

    
189
    if ( mValid )
190
    {
191
      for ( const QString &col : std::as_const( cols ) )
192
      {
193
        const int idx = mAttributeFields.indexFromName( col );
194
        if ( idx < 0 )
195
        {
196
          QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "MSSQL" ) );
197
          mPrimaryKeyType = PktUnknown;
198
          mPrimaryKeyAttrs.clear();
199
          break;
200
        }
201

    
202
        const QgsField &fld = mAttributeFields.at( idx );
203

    
204
        if ( mPrimaryKeyAttrs.size() == 0 && ( fld.type() == QMetaType::Type::Int || fld.type() == QMetaType::Type::LongLong || ( fld.type() == QMetaType::Type::Double && fld.precision() == 0 ) ) )
205
        {
206
          mPrimaryKeyType = PktInt;
207
        }
208
        else
209
        {
210
          mPrimaryKeyType = PktFidMap;
211
        }
212

    
213
        mPrimaryKeyAttrs << idx;
214
      }
215

    
216
      if ( mGeometryColName.isEmpty() )
217
      {
218
        // table contains no geometries
219
        mWkbType = Qgis::WkbType::NoGeometry;
220
        mSRId = 0;
221
      }
222
    }
223
  }
224

    
225
  //fill type names into sets
226
  setNativeTypes( QgsMssqlConnection::nativeTypes() );
227
}
228

    
229
QgsMssqlProvider::~QgsMssqlProvider()
230
{
231
}
232

    
233
QgsAbstractFeatureSource *QgsMssqlProvider::featureSource() const
234
{
235
  return new QgsMssqlFeatureSource( this );
236
}
237

    
238
QgsFeatureIterator QgsMssqlProvider::getFeatures( const QgsFeatureRequest &request ) const
239
{
240
  if ( !mValid )
241
  {
242
    QgsDebugError( QStringLiteral( "Read attempt on an invalid mssql data source" ) );
243
    return QgsFeatureIterator();
244
  }
245

    
246
  return QgsFeatureIterator( new QgsMssqlFeatureIterator( new QgsMssqlFeatureSource( this ), true, request ) );
247
}
248

    
249
QMetaType::Type QgsMssqlProvider::DecodeSqlType( const QString &sqlTypeName )
250
{
251
  QMetaType::Type type = QMetaType::Type::UnknownType;
252
  if ( sqlTypeName.startsWith( QLatin1String( "decimal" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "numeric" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "real" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "float" ), Qt::CaseInsensitive ) )
253
  {
254
    type = QMetaType::Type::Double;
255
  }
256
  else if ( sqlTypeName.startsWith( QLatin1String( "char" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "nchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "varchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "nvarchar" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "text" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "ntext" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "uniqueidentifier" ), Qt::CaseInsensitive ) )
257
  {
258
    type = QMetaType::Type::QString;
259
  }
260
  else if ( sqlTypeName.startsWith( QLatin1String( "smallint" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "int" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "bit" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "tinyint" ), Qt::CaseInsensitive ) )
261
  {
262
    type = QMetaType::Type::Int;
263
  }
264
  else if ( sqlTypeName.startsWith( QLatin1String( "bigint" ), Qt::CaseInsensitive ) )
265
  {
266
    type = QMetaType::Type::LongLong;
267
  }
268
  else if ( sqlTypeName.startsWith( QLatin1String( "binary" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "varbinary" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "image" ), Qt::CaseInsensitive ) )
269
  {
270
    type = QMetaType::Type::QByteArray;
271
  }
272
  else if ( sqlTypeName.startsWith( QLatin1String( "datetime" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "smalldatetime" ), Qt::CaseInsensitive ) || sqlTypeName.startsWith( QLatin1String( "datetime2" ), Qt::CaseInsensitive ) )
273
  {
274
    type = QMetaType::Type::QDateTime;
275
  }
276
  else if ( sqlTypeName.startsWith( QLatin1String( "date" ), Qt::CaseInsensitive ) )
277
  {
278
    type = QMetaType::Type::QDate;
279
  }
280
  else if ( sqlTypeName.startsWith( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) )
281
  {
282
    type = QMetaType::Type::QString;
283
  }
284
  else if ( sqlTypeName.startsWith( QLatin1String( "time" ), Qt::CaseInsensitive ) )
285
  {
286
    type = QMetaType::Type::QTime;
287
  }
288
  else
289
  {
290
    QgsDebugError( QStringLiteral( "Unknown field type: %1" ).arg( sqlTypeName ) );
291
    // Everything else just dumped as a string.
292
    type = QMetaType::Type::QString;
293
  }
294

    
295
  return type;
296
}
297

    
298
void QgsMssqlProvider::loadMetadata()
299
{
300
  mSRId = 0;
301
  mWkbType = Qgis::WkbType::Unknown;
302

    
303
  QSqlQuery query = createQuery();
304
  query.setForwardOnly( true );
305
  if ( !LoggedExec( query, QStringLiteral( "SELECT f_geometry_column, srid, geometry_type, coord_dimension FROM geometry_columns WHERE f_table_schema=%1 AND f_table_name=%2" ).arg( quotedValue( mSchemaName ), quotedValue( mTableName ) ) ) )
306
  {
307
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
308
  }
309

    
310
  if ( query.isActive() && query.next() )
311
  {
312
    mGeometryColName = query.value( 0 ).toString();
313
    mSRId = query.value( 1 ).toInt();
314
    const int dimensions = query.value( 3 ).toInt();
315
    const QString detectedType { QgsMssqlProvider::typeFromMetadata( query.value( 2 ).toString().toUpper(), dimensions ) };
316
    mWkbType = getWkbType( detectedType );
317
  }
318
}
319

    
320
bool QgsMssqlProvider::execLogged( QSqlQuery &qry, const QString &sql, const QString &queryOrigin ) const
321
{
322
  QgsDatabaseQueryLogWrapper logWrapper { sql, uri().uri(), QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), queryOrigin };
323
  const bool res { qry.exec( sql ) };
324
  if ( !res )
325
  {
326
    logWrapper.setError( qry.lastError().text() );
327
  }
328
  else
329
  {
330
    if ( qry.isSelect() )
331
    {
332
      logWrapper.setFetchedRows( qry.size() );
333
    }
334
    else
335
    {
336
      logWrapper.setFetchedRows( qry.numRowsAffected() );
337
    }
338
  }
339
  logWrapper.setQuery( qry.lastQuery() );
340
  return res;
341
}
342

    
343
bool QgsMssqlProvider::execPreparedLogged( QSqlQuery &qry, const QString &queryOrigin ) const
344
{
345
  QgsDatabaseQueryLogWrapper logWrapper { qry.lastQuery(), uri().uri(), QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), queryOrigin };
346
  const bool res { qry.exec() };
347
  if ( !res )
348
  {
349
    logWrapper.setError( qry.lastError().text() );
350
  }
351
  else
352
  {
353
    if ( qry.isSelect() )
354
    {
355
      logWrapper.setFetchedRows( qry.size() );
356
    }
357
    else
358
    {
359
      logWrapper.setFetchedRows( qry.numRowsAffected() );
360
    }
361
  }
362
  logWrapper.setQuery( qry.lastQuery() );
363
  return res;
364
}
365

    
366
void QgsMssqlProvider::setLastError( const QString &error )
367
{
368
  appendError( error );
369
  mLastError = error;
370
}
371

    
372
QSqlQuery QgsMssqlProvider::createQuery() const
373
{
374
  std::shared_ptr<QgsMssqlDatabase> conn = connection();
375
  QSqlDatabase d = conn->db();
376
  if ( !d.isOpen() )
377
  {
378
    QgsDebugError( "Creating query, but the database is not open!" );
379
  }
380
  return QSqlQuery( d );
381
}
382

    
383
void QgsMssqlProvider::loadFields()
384
{
385
  bool isIdentity = false;
386
  mAttributeFields.clear();
387
  mDefaultValues.clear();
388
  mComputedColumns.clear();
389

    
390
  // get field spec
391
  QSqlQuery query = createQuery();
392
  query.setForwardOnly( true );
393

    
394
  const QString sql { QStringLiteral( "SELECT name FROM sys.columns WHERE is_computed = 1 AND object_id = OBJECT_ID('[%1].[%2]')" ).arg( mSchemaName, mTableName ) };
395

    
396
  // Get computed columns which need to be ignored on insert or update.
397
  if ( !LoggedExec( query, sql ) )
398
  {
399
    pushError( query.lastError().text() );
400
    return;
401
  }
402

    
403
  while ( query.next() )
404
  {
405
    mComputedColumns.append( query.value( 0 ).toString() );
406
  }
407

    
408
  // Field has unique constraint
409
  QSet<QString> setColumnUnique;
410
  {
411
    const QString sql2 { QStringLiteral( "SELECT * FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS TC"
412
                                         " INNER JOIN INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE CC ON TC.CONSTRAINT_NAME = CC.CONSTRAINT_NAME"
413
                                         " WHERE TC.CONSTRAINT_SCHEMA = '%1' AND TC.TABLE_NAME = '%2' AND TC.CONSTRAINT_TYPE = 'unique'" )
414
                           .arg( mSchemaName, mTableName ) };
415
    if ( !LoggedExec( query, sql2 ) )
416
    {
417
      pushError( query.lastError().text() );
418
      return;
419
    }
420

    
421
    while ( query.next() )
422
    {
423
      setColumnUnique.insert( query.value( QStringLiteral( "COLUMN_NAME" ) ).toString() );
424
    }
425
  }
426

    
427
  const QString sql3 { QStringLiteral( "exec sp_columns @table_name = N%1, @table_owner = %2" ).arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ) };
428
  if ( !LoggedExec( query, sql3 ) )
429
  {
430
    pushError( query.lastError().text() );
431
    return;
432
  }
433

    
434
  int i = 0;
435
  QStringList pkCandidates;
436
  while ( query.next() )
437
  {
438
    const QString colName = query.value( QStringLiteral( "COLUMN_NAME" ) ).toString();
439
    const QString sqlTypeName = query.value( QStringLiteral( "TYPE_NAME" ) ).toString();
440
    bool columnIsIdentity = false;
441

    
442
    // if we don't have an explicitly set geometry column name, and this is a geometry column, then use it
443
    // but if we DO have an explicitly set geometry column name, then load the other information if this is that column
444
    if ( ( mGeometryColName.isEmpty() && ( sqlTypeName == QLatin1String( "geometry" ) || sqlTypeName == QLatin1String( "geography" ) ) )
445
         || colName == mGeometryColName )
446
    {
447
      mGeometryColName = colName;
448
      mGeometryColType = sqlTypeName;
449
      mParser.mIsGeography = sqlTypeName == QLatin1String( "geography" );
450
    }
451
    else
452
    {
453
      const QMetaType::Type sqlType = DecodeSqlType( sqlTypeName );
454
      if ( sqlTypeName == QLatin1String( "int identity" ) || sqlTypeName == QLatin1String( "bigint identity" ) )
455
      {
456
        mPrimaryKeyType = PktInt;
457
        mPrimaryKeyAttrs << mAttributeFields.size();
458
        columnIsIdentity = true;
459
        isIdentity = true;
460
      }
461
      else if ( sqlTypeName == QLatin1String( "int" ) || sqlTypeName == QLatin1String( "bigint" ) )
462
      {
463
        pkCandidates << colName;
464
      }
465

    
466
      QgsField field;
467
      if ( sqlType == QMetaType::Type::QString )
468
      {
469
        // Field length in chars is column 7 ("Length") of the sp_columns output,
470
        // except for uniqueidentifiers which must use column 6 ("Precision").
471
        int length = query.value( sqlTypeName.startsWith( QStringLiteral( "uniqueidentifier" ), Qt::CaseInsensitive ) ? 6 : 7 ).toInt();
472
        if ( sqlTypeName.startsWith( QLatin1Char( 'n' ) ) )
473
        {
474
          length = length / 2;
475
        }
476
        field = QgsField( colName, sqlType, sqlTypeName, length );
477
      }
478
      else if ( sqlType == QMetaType::Type::Double )
479
      {
480
        field = QgsField( colName, sqlType, sqlTypeName, query.value( QStringLiteral( "PRECISION" ) ).toInt(), sqlTypeName == QLatin1String( "decimal" ) || sqlTypeName == QLatin1String( "numeric" ) ? query.value( QStringLiteral( "SCALE" ) ).toInt() : -1 );
481
      }
482
      else if ( sqlType == QMetaType::Type::QDate || sqlType == QMetaType::Type::QDateTime || sqlType == QMetaType::Type::QTime )
483
      {
484
        field = QgsField( colName, sqlType, sqlTypeName, -1, -1 );
485
      }
486
      else
487
      {
488
        field = QgsField( colName, sqlType, sqlTypeName );
489
      }
490

    
491
      // Field nullable
492
      const bool nullable = query.value( QStringLiteral( "NULLABLE" ) ).toBool();
493

    
494
      // Set constraints
495
      QgsFieldConstraints constraints;
496
      if ( !nullable )
497
        constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider );
498
      if ( setColumnUnique.contains( colName ) )
499
        constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
500
      field.setConstraints( constraints );
501

    
502
      if ( columnIsIdentity )
503
      {
504
        field.setReadOnly( true );
505
      }
506

    
507
      mAttributeFields.append( field );
508

    
509
      // Default value
510
      if ( !QgsVariantUtils::isNull( query.value( QStringLiteral( "COLUMN_DEF" ) ) ) )
511
      {
512
        mDefaultValues.insert( i, query.value( QStringLiteral( "COLUMN_DEF" ) ).toString() );
513
      }
514
      else if ( columnIsIdentity )
515
      {
516
        // identity column types don't report a default value clause in the COLUMN_DEF attribute. So we need to fake
517
        // one, so that we can correctly indicate that the database is responsible for populating this column.
518
        mDefaultValues.insert( i, QStringLiteral( "Autogenerate" ) );
519
      }
520

    
521
      ++i;
522
    }
523
  }
524

    
525
  // get primary key
526
  if ( mPrimaryKeyAttrs.isEmpty() )
527
  {
528
    query.clear();
529
    query.setForwardOnly( true );
530
    const QString sql4 { QStringLiteral( "exec sp_pkeys @table_name = N%1, @table_owner = %2 " ).arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ) };
531
    if ( !LoggedExec( query, sql4 ) )
532
    {
533
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
534
    }
535

    
536
    if ( query.isActive() )
537
    {
538
      mPrimaryKeyType = PktInt;
539

    
540
      while ( query.next() )
541
      {
542
        const QString fidColName = query.value( 3 ).toString();
543
        const int idx = mAttributeFields.indexFromName( fidColName );
544
        const QgsField &fld = mAttributeFields.at( idx );
545

    
546
        if ( !mPrimaryKeyAttrs.isEmpty() || ( fld.type() != QMetaType::Type::Int && fld.type() != QMetaType::Type::LongLong && ( fld.type() != QMetaType::Type::Double || fld.precision() != 0 ) ) )
547
          mPrimaryKeyType = PktFidMap;
548

    
549
        mPrimaryKeyAttrs << idx;
550
      }
551

    
552
      if ( mPrimaryKeyAttrs.isEmpty() )
553
      {
554
        mPrimaryKeyType = PktUnknown;
555
      }
556
    }
557
  }
558

    
559
  if ( mPrimaryKeyAttrs.isEmpty() )
560
  {
561
    const auto constPkCandidates = pkCandidates;
562
    for ( const QString &pk : constPkCandidates )
563
    {
564
      query.clear();
565
      query.setForwardOnly( true );
566
      const QString sql5 { QStringLiteral( "select count(distinct [%1]), count([%1]) from [%2].[%3]" )
567
                             .arg( pk, mSchemaName, mTableName ) };
568
      if ( !LoggedExec( query, sql5 ) )
569
      {
570
        QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
571
      }
572

    
573
      if ( query.isActive() && query.next() && query.value( 0 ).toInt() == query.value( 1 ).toInt() )
574
      {
575
        mPrimaryKeyType = PktInt;
576
        mPrimaryKeyAttrs << mAttributeFields.indexFromName( pk );
577
        return;
578
      }
579
    }
580
  }
581

    
582
  if ( mPrimaryKeyAttrs.isEmpty() )
583
  {
584
    const QString error = QStringLiteral( "No primary key could be found on table %1" ).arg( mTableName );
585
    QgsDebugError( error );
586
    mValid = false;
587
    setLastError( error );
588
  }
589

    
590
  if ( mPrimaryKeyAttrs.size() == 1 && !isIdentity )
591
  {
592
    // primary key has unique constraints
593
    QgsFieldConstraints constraints = mAttributeFields.at( mPrimaryKeyAttrs[0] ).constraints();
594
    constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider );
595
    mAttributeFields[mPrimaryKeyAttrs[0]].setConstraints( constraints );
596
  }
597
}
598

    
599
QString QgsMssqlProvider::quotedValue( const QVariant &value )
600
{
601
  if ( QgsVariantUtils::isNull( value ) )
602
    return QStringLiteral( "NULL" );
603

    
604
  switch ( value.userType() )
605
  {
606
    case QMetaType::Type::Int:
607
    case QMetaType::Type::LongLong:
608
    case QMetaType::Type::Double:
609
      return value.toString();
610

    
611
    case QMetaType::Type::Bool:
612
      return QString( value.toBool() ? '1' : '0' );
613

    
614
    default:
615
    case QMetaType::Type::QString:
616
      QString v = value.toString();
617
      v.replace( '\'', QLatin1String( "''" ) );
618
      if ( v.contains( '\\' ) )
619
        return v.replace( '\\', QLatin1String( "\\\\" ) ).prepend( "N'" ).append( '\'' );
620
      else
621
        return v.prepend( '\'' ).append( '\'' );
622
  }
623
}
624

    
625
QString QgsMssqlProvider::quotedIdentifier( const QString &value )
626
{
627
  return QStringLiteral( "[%1]" ).arg( value );
628
}
629

    
630
QString QgsMssqlProvider::defaultValueClause( int fieldId ) const
631
{
632
  const QString defVal = mDefaultValues.value( fieldId, QString() );
633

    
634
  if ( defVal.isEmpty() )
635
    return QString();
636

    
637
  // NOTE: If EvaluateDefaultValues is activated it is impossible to get the defaultValueClause.
638
  //       This also apply to QgsPostgresProvider::defaultValueClause.
639
  if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
640
    return defVal;
641

    
642
  return QString();
643
}
644

    
645
QVariant QgsMssqlProvider::defaultValue( int fieldId ) const
646
{
647
  const QString defVal = mDefaultValues.value( fieldId, QString() );
648

    
649
  if ( defVal.isEmpty() )
650
    return QVariant();
651

    
652
  if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
653
    return QVariant();
654

    
655
  const QString sql = QStringLiteral( "select %1" )
656
                        .arg( defVal );
657

    
658
  QSqlQuery query = createQuery();
659
  query.setForwardOnly( true );
660

    
661
  if ( !LoggedExec( query, sql ) )
662
  {
663
    const QString errorMessage( tr( "Could not execute query: %1" ).arg( query.lastError().text() ) );
664
    QgsDebugError( errorMessage );
665
    pushError( errorMessage );
666
    return QVariant();
667
  }
668

    
669
  if ( !query.next() )
670
  {
671
    const QString errorMessage( tr( "Could not fetch next query value: %1" ).arg( query.lastError().text() ) );
672
    QgsDebugError( errorMessage );
673
    pushError( errorMessage );
674
    return QVariant();
675
  }
676

    
677
  const QVariant res = query.value( 0 );
678
  return QgsVariantUtils::isNull( res ) ? QVariant() : res;
679
}
680

    
681
QString QgsMssqlProvider::storageType() const
682
{
683
  return QStringLiteral( "MSSQL spatial database" );
684
}
685

    
686
QVariant QgsMssqlProvider::convertTimeValue( const QVariant &value )
687
{
688
  if ( value.isValid() && value.userType() == QMetaType::Type::QByteArray )
689
  {
690
    // time fields can be returned as byte arrays... woot
691
    const QByteArray ba = value.toByteArray();
692
    if ( ba.length() >= 5 )
693
    {
694
      const int hours = ba.at( 0 );
695
      const int mins = ba.at( 2 );
696
      const int seconds = ba.at( 4 );
697
      QVariant t = QTime( hours, mins, seconds );
698
      if ( !t.isValid() ) // can't handle it
699
        t = QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
700
      return t;
701
    }
702
    return QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
703
  }
704
  return value;
705
}
706

    
707
// Returns the minimum value of an attribute
708
QVariant QgsMssqlProvider::minimumValue( int index ) const
709
{
710
  if ( index < 0 || index >= mAttributeFields.count() )
711
  {
712
    return QVariant();
713
  }
714

    
715
  // get the field name
716
  const QgsField &fld = mAttributeFields.at( index );
717
  QString sql = QStringLiteral( "select min([%1]) from " )
718
                  .arg( fld.name() );
719

    
720
  sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName );
721

    
722
  if ( !mSqlWhereClause.isEmpty() )
723
  {
724
    sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause );
725
  }
726

    
727
  QSqlQuery query = createQuery();
728
  query.setForwardOnly( true );
729

    
730
  if ( !LoggedExec( query, sql ) )
731
  {
732
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
733
  }
734

    
735
  if ( query.isActive() && query.next() )
736
  {
737
    QVariant v = query.value( 0 );
738
    if ( fld.type() == QMetaType::Type::QTime )
739
      v = convertTimeValue( v );
740
    if ( v.userType() != fld.type() )
741
      v = convertValue( fld.type(), v.toString() );
742
    return v;
743
  }
744

    
745
  return QVariant( QString() );
746
}
747

    
748
// Returns the maximum value of an attribute
749
QVariant QgsMssqlProvider::maximumValue( int index ) const
750
{
751
  if ( index < 0 || index >= mAttributeFields.count() )
752
  {
753
    return QVariant();
754
  }
755

    
756
  // get the field name
757
  const QgsField &fld = mAttributeFields.at( index );
758
  QString sql = QStringLiteral( "select max([%1]) from " )
759
                  .arg( fld.name() );
760

    
761
  sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName );
762

    
763
  if ( !mSqlWhereClause.isEmpty() )
764
  {
765
    sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause );
766
  }
767

    
768
  QSqlQuery query = createQuery();
769
  query.setForwardOnly( true );
770

    
771
  if ( !LoggedExec( query, sql ) )
772
  {
773
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
774
  }
775

    
776
  if ( query.isActive() && query.next() )
777
  {
778
    QVariant v = query.value( 0 );
779
    if ( fld.type() == QMetaType::Type::QTime )
780
      v = convertTimeValue( v );
781
    if ( v.userType() != fld.type() )
782
      v = convertValue( fld.type(), v.toString() );
783
    return v;
784
  }
785

    
786
  return QVariant( QString() );
787
}
788

    
789
// Returns the list of unique values of an attribute
790
QSet<QVariant> QgsMssqlProvider::uniqueValues( int index, int limit ) const
791
{
792
  QSet<QVariant> uniqueValues;
793
  if ( index < 0 || index >= mAttributeFields.count() )
794
  {
795
    return uniqueValues;
796
  }
797

    
798
  // get the field name
799
  const QgsField &fld = mAttributeFields.at( index );
800
  QString sql = QStringLiteral( "select distinct " );
801

    
802
  if ( limit > 0 )
803
  {
804
    sql += QStringLiteral( " top %1 " ).arg( limit );
805
  }
806

    
807
  sql += QStringLiteral( "[%1] from " )
808
           .arg( fld.name() );
809

    
810
  sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName );
811

    
812
  if ( !mSqlWhereClause.isEmpty() )
813
  {
814
    sql += QStringLiteral( " where (%1)" ).arg( mSqlWhereClause );
815
  }
816

    
817
  QSqlQuery query = createQuery();
818
  query.setForwardOnly( true );
819

    
820
  if ( !LoggedExec( query, sql ) )
821
  {
822
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
823
  }
824

    
825
  if ( query.isActive() )
826
  {
827
    // read all features
828
    while ( query.next() )
829
    {
830
      QVariant v = query.value( 0 );
831
      if ( fld.type() == QMetaType::Type::QTime )
832
        v = convertTimeValue( v );
833
      if ( v.userType() != fld.type() )
834
        v = convertValue( fld.type(), v.toString() );
835
      uniqueValues.insert( v );
836
    }
837
  }
838
  return uniqueValues;
839
}
840

    
841
QStringList QgsMssqlProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
842
{
843
  QStringList results;
844

    
845
  if ( index < 0 || index >= mAttributeFields.count() )
846
  {
847
    return results;
848
  }
849

    
850
  // get the field name
851
  const QgsField &fld = mAttributeFields.at( index );
852
  QString sql = QStringLiteral( "select distinct " );
853

    
854
  if ( limit > 0 )
855
  {
856
    sql += QStringLiteral( " top %1 " ).arg( limit );
857
  }
858

    
859
  sql += QStringLiteral( "[%1] from " )
860
           .arg( fld.name() );
861

    
862
  sql += QStringLiteral( "[%1].[%2] WHERE" ).arg( mSchemaName, mTableName );
863

    
864
  if ( !mSqlWhereClause.isEmpty() )
865
  {
866
    sql += QStringLiteral( " (%1) AND" ).arg( mSqlWhereClause );
867
  }
868

    
869
  sql += QStringLiteral( " [%1] LIKE '%%2%'" ).arg( fld.name(), substring );
870

    
871
  QSqlQuery query = createQuery();
872
  query.setForwardOnly( true );
873

    
874
  if ( !LoggedExec( query, sql ) )
875
  {
876
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
877
  }
878

    
879
  if ( query.isActive() )
880
  {
881
    // read all features
882
    while ( query.next() )
883
    {
884
      results << query.value( 0 ).toString();
885
      if ( feedback && feedback->isCanceled() )
886
        break;
887
    }
888
  }
889
  return results;
890
}
891

    
892
// update the extent, wkb type and srid for this layer, returns false if fails
893
void QgsMssqlProvider::UpdateStatistics( bool estimate ) const
894
{
895
  if ( mGeometryColName.isEmpty() )
896
  {
897
    return;
898
  }
899

    
900
  // get features to calculate the statistics
901
  QString statement;
902

    
903
  QSqlQuery query = createQuery();
904
  query.setForwardOnly( true );
905

    
906

    
907
  if ( mUseGeometryColumnsTableForExtent )
908
  {
909
    if ( !getExtentFromGeometryColumns( mExtent ) )
910
      QgsMessageLog::logMessage( tr( "Invalid extent from geometry_columns table for layer '%1', get extent from the layer." ).arg( mTableName ), tr( "MSSQL" ) );
911
    else
912
      return;
913
  }
914

    
915
  // Get the extents from the spatial index table to speed up load times.
916
  // We have to use max() and min() because you can have more then one index but the biggest area is what we want to use.
917
  const QString sql = "SELECT min(bounding_box_xmin), min(bounding_box_ymin), max(bounding_box_xmax), max(bounding_box_ymax)"
918
                      " FROM sys.spatial_index_tessellations WHERE object_id = OBJECT_ID('[%1].[%2]')";
919

    
920
  statement = QString( sql ).arg( mSchemaName, mTableName );
921

    
922
  if ( LoggedExec( query, statement ) )
923
  {
924
    if ( query.next() && ( !QgsVariantUtils::isNull( query.value( 0 ) ) || !QgsVariantUtils::isNull( query.value( 1 ) ) || !QgsVariantUtils::isNull( query.value( 2 ) ) || !QgsVariantUtils::isNull( query.value( 3 ) ) ) )
925
    {
926
      QgsDebugMsgLevel( QStringLiteral( "Found extents in spatial index" ), 2 );
927
      mExtent.setXMinimum( query.value( 0 ).toDouble() );
928
      mExtent.setYMinimum( query.value( 1 ).toDouble() );
929
      mExtent.setXMaximum( query.value( 2 ).toDouble() );
930
      mExtent.setYMaximum( query.value( 3 ).toDouble() );
931
      return;
932
    }
933
  }
934
  else
935
  {
936
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
937
  }
938

    
939
  // If we can't find the extents in the spatial index table just do what we normally do.
940
  bool readAllGeography = false;
941
  if ( estimate )
942
  {
943
    if ( mGeometryColType == QLatin1String( "geometry" ) )
944
    {
945
      if ( mDisableInvalidGeometryHandling )
946
        statement = QStringLiteral( "select min([%1].STPointN(1).STX), min([%1].STPointN(1).STY), max([%1].STPointN(1).STX), max([%1].STPointN(1).STY)" ).arg( mGeometryColName );
947
      else
948
        statement = QStringLiteral( "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 );
949
    }
950
    else
951
    {
952
      if ( mDisableInvalidGeometryHandling )
953
        statement = QStringLiteral( "select min([%1].STPointN(1).Long), min([%1].STPointN(1).Lat), max([%1].STPointN(1).Long), max([%1].STPointN(1).Lat)" ).arg( mGeometryColName );
954
      else
955
        statement = QStringLiteral( "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 );
956
    }
957

    
958
    // we will first try to sample a small portion of the table/view, so the count of rows involved
959
    // will be useful to evaluate if we have enough data to use the sample
960
    statement += ", count(*)";
961
  }
962
  else
963
  {
964
    if ( mGeometryColType == QLatin1String( "geometry" ) )
965
    {
966
      if ( mDisableInvalidGeometryHandling )
967
        statement = QStringLiteral( "select min([%1].STEnvelope().STPointN(1).STX), min([%1].STEnvelope().STPointN(1).STY), max([%1].STEnvelope().STPointN(3).STX), max([%1].STEnvelope().STPointN(3).STY)" ).arg( mGeometryColName );
968
      else
969
        statement = QStringLiteral( "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 );
970
    }
971
    else
972
    {
973
      statement = QStringLiteral( "select [%1]" ).arg( mGeometryColName );
974
      readAllGeography = true;
975
    }
976
  }
977

    
978
  statement += QStringLiteral( " from [%1].[%2]" ).arg( mSchemaName, mTableName );
979

    
980
  if ( !mSqlWhereClause.isEmpty() )
981
  {
982
    statement += " where (" + mSqlWhereClause + ')';
983
  }
984

    
985
  if ( estimate )
986
  {
987
    // Try to use just 1% sample of the whole table/view to limit the amount of rows accessed.
988
    // This heuristic may fail (e.g. when the table is small or when primary key values do not
989
    // get sampled enough) so in case we do not have at least 10 features, we fall back to full
990
    // traversal of the table/view
991

    
992
    const int minSampleCount = 10;
993

    
994
    QString cols, delim;
995
    for ( const auto idx : mPrimaryKeyAttrs )
996
    {
997
      const QgsField &fld = mAttributeFields.at( idx );
998
      cols += delim + QStringLiteral( "[%1]" ).arg( fld.name() );
999
      delim = QStringLiteral( "," );
1000
    }
1001

    
1002
    // See https://docs.microsoft.com/en-us/previous-versions/software-testing/cc441928(v=msdn.10)
1003
    const QString sampleFilter = QString( "(ABS(CAST((BINARY_CHECKSUM(%1)) as int)) % 100) = 42" ).arg( cols );
1004

    
1005
    const QString statementSample = statement + ( mSqlWhereClause.isEmpty() ? " WHERE " : " AND " ) + sampleFilter;
1006

    
1007
    if ( LoggedExec( query, statementSample ) && query.next() && !QgsVariantUtils::isNull( query.value( 0 ) ) && query.value( 4 ).toInt() >= minSampleCount )
1008
    {
1009
      mExtent.setXMinimum( query.value( 0 ).toDouble() );
1010
      mExtent.setYMinimum( query.value( 1 ).toDouble() );
1011
      mExtent.setXMaximum( query.value( 2 ).toDouble() );
1012
      mExtent.setYMaximum( query.value( 3 ).toDouble() );
1013
      return;
1014
    }
1015
  }
1016

    
1017
  if ( !LoggedExec( query, statement ) )
1018
  {
1019
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1020
  }
1021

    
1022
  if ( !query.isActive() )
1023
  {
1024
    return;
1025
  }
1026

    
1027
  if ( !readAllGeography && query.next() )
1028
  {
1029
    mExtent.setXMinimum( query.value( 0 ).toDouble() );
1030
    mExtent.setYMinimum( query.value( 1 ).toDouble() );
1031
    mExtent.setXMaximum( query.value( 2 ).toDouble() );
1032
    mExtent.setYMaximum( query.value( 3 ).toDouble() );
1033
    return;
1034
  }
1035

    
1036
  // We have to read all the geometry if readAllGeography is true.
1037
  while ( query.next() )
1038
  {
1039
    QByteArray ar = query.value( 0 ).toByteArray();
1040
    std::unique_ptr<QgsAbstractGeometry> geom = mParser.parseSqlGeometry( reinterpret_cast<unsigned char *>( ar.data() ), ar.size() );
1041
    if ( geom )
1042
    {
1043
      const QgsRectangle rect = geom->boundingBox();
1044

    
1045
      if ( rect.xMinimum() < mExtent.xMinimum() )
1046
        mExtent.setXMinimum( rect.xMinimum() );
1047
      if ( rect.yMinimum() < mExtent.yMinimum() )
1048
        mExtent.setYMinimum( rect.yMinimum() );
1049
      if ( rect.xMaximum() > mExtent.xMaximum() )
1050
        mExtent.setXMaximum( rect.xMaximum() );
1051
      if ( rect.yMaximum() > mExtent.yMaximum() )
1052
        mExtent.setYMaximum( rect.yMaximum() );
1053

    
1054
      mWkbType = geom->wkbType();
1055
      mSRId = mParser.GetSRSId();
1056
    }
1057
  }
1058
}
1059

    
1060
// Return the extent of the layer
1061
QgsRectangle QgsMssqlProvider::extent() const
1062
{
1063
  if ( mExtent.isEmpty() )
1064
    UpdateStatistics( mUseEstimatedMetadata );
1065
  return mExtent;
1066
}
1067

    
1068
/**
1069
 * Returns the feature type
1070
 */
1071
Qgis::WkbType QgsMssqlProvider::wkbType() const
1072
{
1073
  return mWkbType;
1074
}
1075

    
1076
/**
1077
 * Returns the feature type
1078
 */
1079
long long QgsMssqlProvider::featureCount() const
1080
{
1081
  // Return the count that we get from the subset.
1082
  if ( !mSqlWhereClause.isEmpty() )
1083
    return mNumberFeatures;
1084

    
1085
  // If there is no subset set we can get the count from the system tables.
1086
  // Which is faster then doing select count(*)
1087
  QSqlQuery query = createQuery();
1088
  query.setForwardOnly( true );
1089

    
1090
  const QString statement = QStringLiteral(
1091
                              "SELECT rows"
1092
                              " FROM sys.tables t"
1093
                              " JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1)"
1094
                              " WHERE SCHEMA_NAME(t.schema_id) = %1 AND OBJECT_NAME(t.OBJECT_ID) = %2"
1095
  )
1096
                              .arg( quotedValue( mSchemaName ), quotedValue( mTableName ) );
1097

    
1098
  if ( LoggedExec( query, statement ) && query.next() )
1099
  {
1100
    return query.value( 0 ).toLongLong();
1101
  }
1102
  else
1103
  {
1104
    // We couldn't get the rows from the sys tables. Can that ever happen?
1105
    // Should just do a select count(*) here.
1106
    return -1;
1107
  }
1108
}
1109

    
1110
QgsFields QgsMssqlProvider::fields() const
1111
{
1112
  return mAttributeFields;
1113
}
1114

    
1115
bool QgsMssqlProvider::isValid() const
1116
{
1117
  return mValid;
1118
}
1119

    
1120
Qgis::ProviderStyleStorageCapabilities QgsMssqlProvider::styleStorageCapabilities() const
1121
{
1122
  Qgis::ProviderStyleStorageCapabilities storageCapabilities;
1123
  if ( isValid() )
1124
  {
1125
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
1126
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
1127
  }
1128
  return storageCapabilities;
1129
}
1130

    
1131
bool QgsMssqlProvider::addFeatures( QgsFeatureList &flist, Flags flags )
1132
{
1133
  for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it )
1134
  {
1135
    if ( it->hasGeometry() && mWkbType == Qgis::WkbType::NoGeometry )
1136
    {
1137
      it->clearGeometry();
1138
    }
1139
    else if ( it->hasGeometry() && QgsWkbTypes::geometryType( it->geometry().wkbType() ) != QgsWkbTypes::geometryType( mWkbType ) )
1140
    {
1141
      pushError( tr( "Could not add feature with geometry type %1 to layer of type %2" ).arg( QgsWkbTypes::displayString( it->geometry().wkbType() ), QgsWkbTypes::displayString( mWkbType ) ) );
1142
      if ( !mSkipFailures )
1143
        return false;
1144

    
1145
      continue;
1146
    }
1147

    
1148
    QString statement;
1149
    QString values;
1150
    if ( !( flags & QgsFeatureSink::FastInsert ) )
1151
    {
1152
      statement += QLatin1String( "DECLARE @px TABLE (" );
1153

    
1154
      QString delim;
1155
      for ( const auto idx : mPrimaryKeyAttrs )
1156
      {
1157
        const QgsField &fld = mAttributeFields.at( idx );
1158

    
1159
        QString type = fld.typeName();
1160
        if ( type.endsWith( QLatin1String( " identity" ) ) )
1161
          type = type.left( type.length() - 9 );
1162
        if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) )
1163
        {
1164
          if ( fld.length() > 0 )
1165
            type = QStringLiteral( "%1(%2)" ).arg( type ).arg( fld.length() );
1166
        }
1167
        else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
1168
        {
1169
          if ( fld.length() > 0 && fld.precision() > 0 )
1170
            type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( fld.length() ).arg( fld.precision() );
1171
        }
1172

    
1173
        statement += delim + QStringLiteral( "[%1] %2" ).arg( fld.name(), type );
1174
        delim = ",";
1175
      }
1176

    
1177
      statement += "); ";
1178
    }
1179

    
1180
    statement += QStringLiteral( "INSERT INTO [%1].[%2] (" ).arg( mSchemaName, mTableName );
1181

    
1182
    bool first = true;
1183
    QSqlQuery query = createQuery();
1184
    query.setForwardOnly( true );
1185

    
1186
    const QgsAttributes attrs = it->attributes();
1187

    
1188
    for ( int i = 0; i < attrs.count(); ++i )
1189
    {
1190
      if ( i >= mAttributeFields.count() )
1191
        break;
1192

    
1193
      const QgsField &fld = mAttributeFields.at( i );
1194

    
1195
      if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
1196
        continue; // You can't update timestamp columns they are server only.
1197

    
1198
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1199
        continue; // skip identity field
1200

    
1201
      if ( fld.name().isEmpty() )
1202
        continue; // invalid
1203

    
1204
      if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
1205
        continue; // skip fields having default values
1206

    
1207
      if ( mComputedColumns.contains( fld.name() ) )
1208
        continue; // skip computed columns because they are done server side.
1209

    
1210
      if ( !first )
1211
      {
1212
        statement += ',';
1213
        values += ',';
1214
      }
1215
      else
1216
        first = false;
1217

    
1218
      statement += QStringLiteral( "[%1]" ).arg( fld.name() );
1219
      values += QLatin1Char( '?' );
1220
    }
1221

    
1222
    // append geometry column name
1223
    if ( !mGeometryColName.isEmpty() )
1224
    {
1225
      if ( !first )
1226
      {
1227
        statement += ',';
1228
        values += ',';
1229
      }
1230

    
1231
      statement += QStringLiteral( "[%1]" ).arg( mGeometryColName );
1232
      if ( mGeometryColType == QLatin1String( "geometry" ) )
1233
      {
1234
        if ( mUseWkb )
1235
          values += QStringLiteral( "geometry::STGeomFromWKB(?,%1).MakeValid()" ).arg( mSRId );
1236
        else
1237
          values += QStringLiteral( "geometry::STGeomFromText(?,%1).MakeValid()" ).arg( mSRId );
1238
      }
1239
      else
1240
      {
1241
        if ( mUseWkb )
1242
          values += QStringLiteral( "geography::STGeomFromWKB(?,%1)" ).arg( mSRId );
1243
        else
1244
          values += QStringLiteral( "geography::STGeomFromText(?,%1)" ).arg( mSRId );
1245
      }
1246
    }
1247

    
1248
    statement += QLatin1String( ") " );
1249
    if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
1250
    {
1251
      statement += QLatin1String( " OUTPUT " );
1252

    
1253
      QString delim;
1254

    
1255
      for ( const auto idx : std::as_const( mPrimaryKeyAttrs ) )
1256
      {
1257
        const QgsField &fld = mAttributeFields.at( idx );
1258
        statement += delim + QStringLiteral( "inserted.[%1]" ).arg( fld.name() );
1259
        delim = QStringLiteral( "," );
1260
      }
1261

    
1262
      statement += QLatin1String( " INTO @px " );
1263
    }
1264

    
1265
    statement += QStringLiteral( " VALUES (" ) + values + ')';
1266

    
1267
    if ( !( flags & QgsFeatureSink::FastInsert && !mPrimaryKeyAttrs.isEmpty() ) )
1268
    {
1269
      statement += QLatin1String( "; SELECT * FROM @px;" );
1270
    }
1271

    
1272
    // use prepared statement to prevent from sql injection
1273
    if ( !query.prepare( statement ) )
1274
    {
1275
      const QString msg = query.lastError().text();
1276
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1277
      if ( !mSkipFailures )
1278
      {
1279
        pushError( msg );
1280
        return false;
1281
      }
1282
      else
1283
        continue;
1284
    }
1285

    
1286
    for ( int i = 0; i < attrs.count(); ++i )
1287
    {
1288
      if ( i >= mAttributeFields.count() )
1289
        break;
1290

    
1291
      const QgsField &fld = mAttributeFields.at( i );
1292

    
1293
      if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
1294
        continue; // You can't update timestamp columns they are server only.
1295

    
1296
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1297
        continue; // skip identity field
1298

    
1299
      if ( fld.name().isEmpty() )
1300
        continue; // invalid
1301

    
1302
      if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
1303
        continue; // skip fields having default values
1304

    
1305
      if ( mComputedColumns.contains( fld.name() ) )
1306
        continue; // skip computed columns because they are done server side.
1307

    
1308
      const QMetaType::Type type = fld.type();
1309
      if ( QgsVariantUtils::isNull( attrs.at( i ) ) )
1310
      {
1311
        // binding null values
1312
        if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
1313
          query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
1314
        else
1315
          query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
1316
      }
1317
      else if ( type == QMetaType::Type::Int )
1318
      {
1319
        // binding an INTEGER value
1320
        query.addBindValue( attrs.at( i ).toInt() );
1321
      }
1322
      else if ( type == QMetaType::Type::Double )
1323
      {
1324
        // binding a DOUBLE value
1325
        query.addBindValue( attrs.at( i ).toDouble() );
1326
      }
1327
      else if ( type == QMetaType::Type::QString )
1328
      {
1329
        // binding a TEXT value
1330
        query.addBindValue( attrs.at( i ).toString() );
1331
      }
1332
      else if ( type == QMetaType::Type::QTime )
1333
      {
1334
        // binding a TIME value
1335
        query.addBindValue( attrs.at( i ).toTime().toString( Qt::ISODate ) );
1336
      }
1337
      else if ( type == QMetaType::Type::QDate )
1338
      {
1339
        // binding a DATE value
1340
        query.addBindValue( attrs.at( i ).toDate().toString( Qt::ISODate ) );
1341
      }
1342
      else if ( type == QMetaType::Type::QDateTime )
1343
      {
1344
        // binding a DATETIME value
1345
        query.addBindValue( attrs.at( i ).toDateTime().toString( Qt::ISODate ) );
1346
      }
1347
      else
1348
      {
1349
        query.addBindValue( attrs.at( i ) );
1350
      }
1351
    }
1352

    
1353
    if ( !mGeometryColName.isEmpty() )
1354
    {
1355
      QgsGeometry geom = it->geometry();
1356
      if ( QgsWkbTypes::isMultiType( mWkbType ) && !geom.isMultipart() )
1357
      {
1358
        geom.convertToMultiType();
1359
      }
1360
      if ( mUseWkb )
1361
      {
1362
        const QByteArray bytea = geom.asWkb();
1363
        query.addBindValue( bytea, QSql::In | QSql::Binary );
1364
      }
1365
      else
1366
      {
1367
        QString wkt;
1368
        if ( !geom.isNull() )
1369
        {
1370
          // Z and M on the end of a WKT string isn't valid for
1371
          // SQL Server so we have to remove it first.
1372
          wkt = geom.asWkt();
1373
          const thread_local QRegularExpression wktRx( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
1374
          wkt.replace( wktRx, QStringLiteral( "(" ) );
1375
          // if we have M value only, we need to insert null-s for the Z value
1376
          if ( QgsWkbTypes::hasM( geom.wkbType() ) && !QgsWkbTypes::hasZ( geom.wkbType() ) )
1377
          {
1378
            const thread_local QRegularExpression nullRx( QStringLiteral( "(?=\\s[0-9+-.]+[,)])" ) );
1379
            wkt.replace( QRegularExpression( nullRx ), QStringLiteral( " NULL" ) );
1380
          }
1381
        }
1382
        query.addBindValue( wkt );
1383
      }
1384
    }
1385

    
1386
    if ( !LoggedExecPrepared( query ) )
1387
    {
1388
      const QString msg = query.lastError().text();
1389
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1390
      if ( !mSkipFailures )
1391
      {
1392
        pushError( msg );
1393
        return false;
1394
      }
1395
    }
1396

    
1397
    if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
1398
    {
1399
      if ( !query.next() )
1400
      {
1401
        const QString msg = query.lastError().text();
1402
        QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1403
        if ( !mSkipFailures )
1404
        {
1405
          pushError( msg );
1406
          return false;
1407
        }
1408
      }
1409

    
1410
      if ( mPrimaryKeyType == PktInt )
1411
      {
1412
        it->setId( query.value( 0 ).toLongLong() );
1413
      }
1414
      else
1415
      {
1416
        QVariantList keyvals;
1417
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
1418
        {
1419
          keyvals << query.value( i );
1420
        }
1421

    
1422
        it->setId( mShared->lookupFid( keyvals ) );
1423
      }
1424
    }
1425
  }
1426

    
1427
  if ( mTransaction )
1428
    mTransaction->dirtyLastSavePoint();
1429

    
1430
  return true;
1431
}
1432

    
1433
bool QgsMssqlProvider::addAttributes( const QList<QgsField> &attributes )
1434
{
1435
  QString statement;
1436

    
1437
  if ( attributes.isEmpty() )
1438
    return true;
1439

    
1440
  for ( QList<QgsField>::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
1441
  {
1442
    QString type = it->typeName();
1443
    if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) || type == QLatin1String( "nvarchar" ) )
1444
    {
1445
      if ( it->length() > 0 )
1446
        type = QStringLiteral( "%1(%2)" ).arg( type ).arg( it->length() );
1447
    }
1448
    else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
1449
    {
1450
      if ( it->length() > 0 && it->precision() > 0 )
1451
        type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( it->length() ).arg( it->precision() );
1452
    }
1453

    
1454
    if ( statement.isEmpty() )
1455
    {
1456
      statement = QStringLiteral( "ALTER TABLE [%1].[%2] ADD " ).arg( mSchemaName, mTableName );
1457
    }
1458
    else
1459
      statement += ',';
1460

    
1461
    statement += QStringLiteral( "[%1] %2" ).arg( it->name(), type );
1462
  }
1463

    
1464
  QSqlQuery query = createQuery();
1465
  query.setForwardOnly( true );
1466
  if ( !LoggedExec( query, statement ) )
1467
  {
1468
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1469
    return false;
1470
  }
1471

    
1472
  loadFields();
1473

    
1474
  if ( mTransaction )
1475
    mTransaction->dirtyLastSavePoint();
1476

    
1477
  return true;
1478
}
1479

    
1480
bool QgsMssqlProvider::deleteAttributes( const QgsAttributeIds &attributes )
1481
{
1482
  QString statement;
1483

    
1484
  for ( QgsAttributeIds::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
1485
  {
1486
    if ( statement.isEmpty() )
1487
    {
1488
      statement = QStringLiteral( "ALTER TABLE [%1].[%2] DROP COLUMN " ).arg( mSchemaName, mTableName );
1489
    }
1490
    else
1491
      statement += ',';
1492

    
1493
    statement += QStringLiteral( "[%1]" ).arg( mAttributeFields.at( *it ).name() );
1494
  }
1495

    
1496
  QSqlQuery query = createQuery();
1497
  query.setForwardOnly( true );
1498

    
1499
  if ( !LoggedExec( query, statement ) )
1500
  {
1501
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1502
    return false;
1503
  }
1504

    
1505
  query.finish();
1506

    
1507
  loadFields();
1508

    
1509
  if ( mTransaction )
1510
    mTransaction->dirtyLastSavePoint();
1511

    
1512
  return true;
1513
}
1514

    
1515

    
1516
bool QgsMssqlProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
1517
{
1518
  if ( attr_map.isEmpty() )
1519
    return true;
1520

    
1521
  if ( mPrimaryKeyAttrs.isEmpty() )
1522
    return false;
1523

    
1524
  for ( QgsChangedAttributesMap::const_iterator it = attr_map.begin(); it != attr_map.end(); ++it )
1525
  {
1526
    const QgsFeatureId fid = it.key();
1527

    
1528
    // skip added features
1529
    if ( FID_IS_NEW( fid ) )
1530
      continue;
1531

    
1532
    const QgsAttributeMap &attrs = it.value();
1533
    if ( attrs.isEmpty() )
1534
      continue;
1535

    
1536
    QString statement = QStringLiteral( "UPDATE [%1].[%2] SET " ).arg( mSchemaName, mTableName );
1537

    
1538
    bool first = true;
1539
    bool pkChanged = false;
1540
    QSqlQuery query = createQuery();
1541
    query.setForwardOnly( true );
1542

    
1543
    for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
1544
    {
1545
      const QgsField fld = mAttributeFields.at( it2.key() );
1546

    
1547
      if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
1548
        continue; // You can't update timestamp columns they are server only.
1549

    
1550
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1551
        continue; // skip identity field
1552

    
1553
      if ( fld.name().isEmpty() )
1554
        continue; // invalid
1555

    
1556
      if ( mComputedColumns.contains( fld.name() ) )
1557
        continue; // skip computed columns because they are done server side.
1558

    
1559
      if ( !first )
1560
        statement += ',';
1561
      else
1562
        first = false;
1563

    
1564
      pkChanged = pkChanged || mPrimaryKeyAttrs.contains( it2.key() );
1565

    
1566
      statement += QStringLiteral( "[%1]=?" ).arg( fld.name() );
1567
    }
1568

    
1569
    if ( first )
1570
      return true; // no fields have been changed
1571

    
1572
    // set attribute filter
1573
    statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
1574

    
1575
    // use prepared statement to prevent from sql injection
1576
    if ( !query.prepare( statement ) )
1577
    {
1578
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1579
      return false;
1580
    }
1581

    
1582
    for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
1583
    {
1584
      const QgsField fld = mAttributeFields.at( it2.key() );
1585

    
1586
      if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
1587
        continue; // You can't update timestamp columns they are server only.
1588

    
1589
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1590
        continue; // skip identity field
1591

    
1592
      if ( fld.name().isEmpty() )
1593
        continue; // invalid
1594

    
1595
      if ( mComputedColumns.contains( fld.name() ) )
1596
        continue; // skip computed columns because they are done server side.
1597

    
1598
      const QMetaType::Type type = fld.type();
1599
      if ( QgsVariantUtils::isNull( *it2 ) )
1600
      {
1601
        // binding null values
1602
        if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
1603
          query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
1604
        else
1605
          query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
1606
      }
1607
      else if ( type == QMetaType::Type::Int )
1608
      {
1609
        // binding an INTEGER value
1610
        query.addBindValue( it2->toInt() );
1611
      }
1612
      else if ( type == QMetaType::Type::Double )
1613
      {
1614
        // binding a DOUBLE value
1615
        query.addBindValue( it2->toDouble() );
1616
      }
1617
      else if ( type == QMetaType::Type::QString )
1618
      {
1619
        // binding a TEXT value
1620
        query.addBindValue( it2->toString() );
1621
      }
1622
      else if ( type == QMetaType::Type::QDateTime )
1623
      {
1624
        // binding a DATETIME value
1625
        query.addBindValue( it2->toDateTime().toString( Qt::ISODate ) );
1626
      }
1627
      else if ( type == QMetaType::Type::QDate )
1628
      {
1629
        // binding a DATE value
1630
        query.addBindValue( it2->toDate().toString( Qt::ISODate ) );
1631
      }
1632
      else if ( type == QMetaType::Type::QTime )
1633
      {
1634
        // binding a TIME value
1635
        query.addBindValue( it2->toTime().toString( Qt::ISODate ) );
1636
      }
1637
      else
1638
      {
1639
        query.addBindValue( *it2 );
1640
      }
1641
    }
1642

    
1643
    if ( !LoggedExecPrepared( query ) )
1644
    {
1645
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1646
      return false;
1647
    }
1648

    
1649
    if ( pkChanged && mPrimaryKeyType == PktFidMap )
1650
    {
1651
      const QVariant v = mShared->removeFid( fid );
1652
      QVariantList k = v.toList();
1653

    
1654
      for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
1655
      {
1656
        const int idx = mPrimaryKeyAttrs[i];
1657
        if ( !attrs.contains( idx ) )
1658
          continue;
1659

    
1660
        k[i] = attrs[idx];
1661
      }
1662

    
1663
      mShared->insertFid( fid, k );
1664
    }
1665
  }
1666

    
1667
  if ( mTransaction )
1668
    mTransaction->dirtyLastSavePoint();
1669

    
1670
  return true;
1671
}
1672

    
1673
bool QgsMssqlProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
1674
{
1675
  if ( geometry_map.isEmpty() )
1676
    return true;
1677

    
1678
  if ( mPrimaryKeyAttrs.isEmpty() )
1679
    return false;
1680

    
1681
  for ( QgsGeometryMap::const_iterator it = geometry_map.constBegin(); it != geometry_map.constEnd(); ++it )
1682
  {
1683
    const QgsFeatureId fid = it.key();
1684
    // skip added features
1685
    if ( FID_IS_NEW( fid ) )
1686
      continue;
1687

    
1688
    QString statement;
1689
    statement = QStringLiteral( "UPDATE [%1].[%2] SET " ).arg( mSchemaName, mTableName );
1690

    
1691
    QSqlQuery query = createQuery();
1692
    query.setForwardOnly( true );
1693

    
1694
    if ( mGeometryColType == QLatin1String( "geometry" ) )
1695
    {
1696
      if ( mUseWkb )
1697
        statement += QStringLiteral( "[%1]=geometry::STGeomFromWKB(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
1698
      else
1699
        statement += QStringLiteral( "[%1]=geometry::STGeomFromText(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
1700
    }
1701
    else
1702
    {
1703
      if ( mUseWkb )
1704
        statement += QStringLiteral( "[%1]=geography::STGeomFromWKB(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
1705
      else
1706
        statement += QStringLiteral( "[%1]=geography::STGeomFromText(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
1707
    }
1708

    
1709
    // set attribute filter
1710
    statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
1711

    
1712
    if ( !query.prepare( statement ) )
1713
    {
1714
      pushError( query.lastError().text() );
1715
      return false;
1716
    }
1717

    
1718
    // add geometry param
1719
    if ( mUseWkb )
1720
    {
1721
      const QByteArray bytea = it->asWkb();
1722
      query.addBindValue( bytea, QSql::In | QSql::Binary );
1723
    }
1724
    else
1725
    {
1726
      QString wkt = it->asWkt();
1727
      // Z and M on the end of a WKT string isn't valid for
1728
      // SQL Server so we have to remove it first.
1729
      const thread_local QRegularExpression zmRegExp( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
1730
      wkt.replace( zmRegExp, QStringLiteral( "(" ) );
1731
      query.addBindValue( wkt );
1732
    }
1733

    
1734
    if ( !LoggedExecPrepared( query ) )
1735
    {
1736
      pushError( query.lastError().text() );
1737
      return false;
1738
    }
1739
  }
1740

    
1741
  if ( mTransaction )
1742
    mTransaction->dirtyLastSavePoint();
1743

    
1744
  return true;
1745
}
1746

    
1747
bool QgsMssqlProvider::deleteFeatures( const QgsFeatureIds &ids )
1748
{
1749
  if ( mPrimaryKeyAttrs.isEmpty() )
1750
    return false;
1751

    
1752
  if ( ids.empty() )
1753
    return true; // for consistency providers return true to an empty list
1754

    
1755
  if ( mPrimaryKeyType == PktInt )
1756
  {
1757
    QString featureIds, delim;
1758
    for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
1759
    {
1760
      featureIds += delim + FID_TO_STRING( *it );
1761
      delim = QStringLiteral( "," );
1762
    }
1763

    
1764
    QSqlQuery query = createQuery();
1765
    query.setForwardOnly( true );
1766

    
1767
    const QString statement = QStringLiteral( "DELETE FROM [%1].[%2] WHERE [%3] IN (%4)" ).arg( mSchemaName, mTableName, mAttributeFields.at( mPrimaryKeyAttrs[0] ).name(), featureIds );
1768

    
1769
    if ( LoggedExec( query, statement ) )
1770
    {
1771
      if ( query.numRowsAffected() == ids.size() )
1772
      {
1773
        if ( mTransaction )
1774
          mTransaction->dirtyLastSavePoint();
1775
        return true;
1776
      }
1777

    
1778
      pushError( tr( "Only %1 of %2 features deleted" ).arg( query.numRowsAffected() ).arg( ids.size() ) );
1779
    }
1780
    else
1781
    {
1782
      pushError( query.lastError().text() );
1783
    }
1784
  }
1785
  else if ( mPrimaryKeyType == PktFidMap )
1786
  {
1787
    int i = 0;
1788

    
1789
    QSqlQuery query = createQuery();
1790
    for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
1791
    {
1792
      const QString statement = QStringLiteral( "DELETE FROM [%1].[%2] WHERE %3" ).arg( mSchemaName, mTableName, whereClauseFid( *it ) );
1793

    
1794
      if ( LoggedExec( query, statement ) )
1795
      {
1796
        if ( query.numRowsAffected() == 1 )
1797
        {
1798
          mShared->removeFid( *it );
1799
          i++;
1800
        }
1801
      }
1802
      else
1803
      {
1804
        pushError( query.lastError().text() );
1805
        break;
1806
      }
1807
    }
1808

    
1809
    if ( i == ids.size() )
1810
    {
1811
      if ( mTransaction )
1812
        mTransaction->dirtyLastSavePoint();
1813
      return true;
1814
    }
1815

    
1816
    if ( i > 0 )
1817
      pushError( tr( "Only %1 of %2 features deleted" ).arg( i ).arg( ids.size() ) );
1818
  }
1819

    
1820
  return false;
1821
}
1822

    
1823
void QgsMssqlProvider::updateExtents()
1824
{
1825
  mExtent.setNull();
1826
}
1827

    
1828
Qgis::VectorProviderCapabilities QgsMssqlProvider::capabilities() const
1829
{
1830
  Qgis::VectorProviderCapabilities cap = Qgis::VectorProviderCapability::CreateAttributeIndex | Qgis::VectorProviderCapability::AddFeatures | Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::TransactionSupport;
1831
  bool hasGeom = false;
1832
  if ( !mGeometryColName.isEmpty() )
1833
  {
1834
    hasGeom = true;
1835
    cap |= Qgis::VectorProviderCapability::CreateSpatialIndex;
1836
  }
1837

    
1838
  if ( mPrimaryKeyAttrs.isEmpty() )
1839
    return cap;
1840

    
1841
  if ( hasGeom )
1842
    cap |= Qgis::VectorProviderCapability::ChangeGeometries;
1843

    
1844
  return cap | Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::DeleteAttributes | Qgis::VectorProviderCapability::SelectAtId;
1845
}
1846

    
1847
bool QgsMssqlProvider::createSpatialIndex()
1848
{
1849
  if ( mUseEstimatedMetadata )
1850
    UpdateStatistics( false );
1851

    
1852
  QSqlQuery query = createQuery();
1853
  query.setForwardOnly( true );
1854
  QString statement;
1855
  statement = QStringLiteral( "CREATE SPATIAL INDEX [qgs_%1_sidx] ON [%2].[%3] ( [%4] )" ).arg( mGeometryColName, mSchemaName, mTableName, mGeometryColName );
1856

    
1857
  if ( mGeometryColType == QLatin1String( "geometry" ) )
1858
  {
1859
    statement += QStringLiteral( " USING GEOMETRY_GRID WITH (BOUNDING_BOX =(%1, %2, %3, %4))" ).arg( QString::number( mExtent.xMinimum() ), QString::number( mExtent.yMinimum() ), QString::number( mExtent.xMaximum() ), QString::number( mExtent.yMaximum() ) );
1860
  }
1861
  else
1862
  {
1863
    statement += QLatin1String( " USING GEOGRAPHY_GRID" );
1864
  }
1865

    
1866
  if ( !LoggedExec( query, statement ) )
1867
  {
1868
    pushError( query.lastError().text() );
1869
    return false;
1870
  }
1871

    
1872
  return true;
1873
}
1874

    
1875
bool QgsMssqlProvider::createAttributeIndex( int field )
1876
{
1877
  QSqlQuery query = createQuery();
1878
  query.setForwardOnly( true );
1879
  QString statement;
1880

    
1881
  if ( field < 0 || field >= mAttributeFields.size() )
1882
  {
1883
    pushError( QStringLiteral( "createAttributeIndex invalid index" ) );
1884
    return false;
1885
  }
1886

    
1887
  statement = QStringLiteral( "CREATE NONCLUSTERED INDEX [qgs_%1_idx] ON [%2].[%3] ( [%4] )" ).arg( mGeometryColName, mSchemaName, mTableName, mAttributeFields.at( field ).name() );
1888

    
1889
  if ( !LoggedExec( query, statement ) )
1890
  {
1891
    pushError( query.lastError().text() );
1892
    return false;
1893
  }
1894

    
1895
  return true;
1896
}
1897

    
1898
QgsCoordinateReferenceSystem QgsMssqlProvider::crs() const
1899
{
1900
  if ( !mCrs.isValid() && mSRId > 0 )
1901
  {
1902
    // try to load crs from the database tables as a fallback
1903
    QSqlQuery query = createQuery();
1904
    query.setForwardOnly( true );
1905
    const QString statement { QStringLiteral( "SELECT srtext FROM spatial_ref_sys WHERE srid=%1" ).arg( mSRId ) };
1906

    
1907
    bool execOk = LoggedExec( query, statement );
1908
    if ( execOk && query.isActive() )
1909
    {
1910
      if ( query.next() )
1911
      {
1912
        mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
1913
        if ( mCrs.isValid() )
1914
          return mCrs;
1915
      }
1916

    
1917
      query.finish();
1918
    }
1919
    query.clear();
1920
    query.setForwardOnly( true );
1921

    
1922
    // Look in the system reference table for the data if we can't find it yet
1923
    execOk = LoggedExec( query, QStringLiteral( "SELECT well_known_text FROM sys.spatial_reference_systems WHERE spatial_reference_id=%1" ).arg( mSRId ) );
1924
    if ( execOk && query.isActive() && query.next() )
1925
    {
1926
      mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
1927
      if ( mCrs.isValid() )
1928
        return mCrs;
1929
    }
1930
    else // try to load as EPSG
1931
    {
1932
      mCrs = QgsCoordinateReferenceSystem::fromEpsgId( mSRId );
1933
    }
1934
  }
1935
  return mCrs;
1936
}
1937

    
1938

    
1939
void QgsMssqlProvider::setTransaction( QgsTransaction *transaction )
1940
{
1941
  // static_cast since layers cannot be added to a transaction of a non-matching provider
1942
  mTransaction = static_cast<QgsMssqlTransaction *>( transaction );
1943
}
1944

    
1945
QgsTransaction *QgsMssqlProvider::transaction() const
1946
{
1947
  return mTransaction;
1948
}
1949

    
1950
std::shared_ptr<QgsMssqlDatabase> QgsMssqlProvider::connection() const
1951
{
1952
  return mTransaction ? mTransaction->conn() : QgsMssqlDatabase::connectDb( uri().connectionInfo(), false );
1953
}
1954

    
1955
void QgsMssqlProvider::handlePostCloneOperations( QgsVectorDataProvider *source )
1956
{
1957
  mShared = qobject_cast<QgsMssqlProvider *>( source )->mShared;
1958
}
1959

    
1960
QString QgsMssqlProvider::subsetString() const
1961
{
1962
  return mSqlWhereClause;
1963
}
1964

    
1965
QString QgsMssqlProvider::name() const
1966
{
1967
  return MSSQL_PROVIDER_KEY;
1968
}
1969

    
1970
bool QgsMssqlProvider::setSubsetString( const QString &theSQL, bool )
1971
{
1972
  if ( theSQL.trimmed() == mSqlWhereClause )
1973
    return true;
1974

    
1975
  const QString prevWhere = mSqlWhereClause;
1976

    
1977
  mSqlWhereClause = theSQL.trimmed();
1978

    
1979
  QString sql = QStringLiteral( "SELECT count(*) FROM " );
1980

    
1981
  sql += QStringLiteral( "[%1].[%2]" ).arg( mSchemaName, mTableName );
1982

    
1983
  if ( !mSqlWhereClause.isEmpty() )
1984
  {
1985
    sql += QStringLiteral( " WHERE %1" ).arg( mSqlWhereClause );
1986
  }
1987

    
1988
  QSqlQuery query = createQuery();
1989
  query.setForwardOnly( true );
1990
  if ( !LoggedExec( query, sql ) )
1991
  {
1992
    pushError( query.lastError().text() );
1993
    mSqlWhereClause = prevWhere;
1994
    return false;
1995
  }
1996

    
1997
  if ( query.isActive() && query.next() )
1998
    mNumberFeatures = query.value( 0 ).toLongLong();
1999

    
2000
  QgsDataSourceUri anUri = QgsDataSourceUri( dataSourceUri() );
2001
  anUri.setSql( mSqlWhereClause );
2002

    
2003
  setDataSourceUri( anUri.uri() );
2004

    
2005
  mExtent.setNull();
2006

    
2007
  emit dataChanged();
2008

    
2009
  return true;
2010
}
2011

    
2012
bool QgsMssqlProvider::supportsSubsetString() const
2013
{
2014
  return true;
2015
}
2016

    
2017
QString QgsMssqlProvider::subsetStringDialect() const
2018
{
2019
  return tr( "SQL Server WHERE clause" );
2020
}
2021

    
2022
QString QgsMssqlProvider::subsetStringHelpUrl() const
2023
{
2024
  return QStringLiteral( "https://learn.microsoft.com/en-us/sql/t-sql/queries/where-transact-sql?view=sql-server-ver16" );
2025
}
2026

    
2027
QString QgsMssqlProvider::description() const
2028
{
2029
  return MSSQL_PROVIDER_DESCRIPTION;
2030
}
2031

    
2032
QgsAttributeList QgsMssqlProvider::pkAttributeIndexes() const
2033
{
2034
  return mPrimaryKeyAttrs;
2035
}
2036

    
2037
QStringList QgsMssqlProvider::subLayers() const
2038
{
2039
  return mTables;
2040
}
2041

    
2042
bool QgsMssqlProvider::convertField( QgsField &field )
2043
{
2044
  QString fieldType = QStringLiteral( "nvarchar(max)" ); //default to string
2045
  int fieldSize = field.length();
2046
  int fieldPrec = field.precision();
2047
  switch ( field.type() )
2048
  {
2049
    case QMetaType::Type::LongLong:
2050
      fieldType = QStringLiteral( "bigint" );
2051
      fieldSize = -1;
2052
      fieldPrec = 0;
2053
      break;
2054

    
2055
    case QMetaType::Type::QDateTime:
2056
      fieldType = QStringLiteral( "datetime" );
2057
      fieldPrec = 0;
2058
      break;
2059

    
2060
    case QMetaType::Type::QDate:
2061
      fieldType = QStringLiteral( "date" );
2062
      fieldPrec = 0;
2063
      break;
2064

    
2065
    case QMetaType::Type::QTime:
2066
      fieldType = QStringLiteral( "time" );
2067
      fieldPrec = 0;
2068
      break;
2069

    
2070
    case QMetaType::Type::QString:
2071
      fieldType = QStringLiteral( "nvarchar(max)" );
2072
      fieldPrec = 0;
2073
      break;
2074

    
2075
    case QMetaType::Type::Int:
2076
      fieldType = QStringLiteral( "int" );
2077
      fieldSize = -1;
2078
      fieldPrec = 0;
2079
      break;
2080

    
2081
    case QMetaType::Type::Double:
2082
      if ( fieldSize <= 0 || fieldPrec <= 0 )
2083
      {
2084
        fieldType = QStringLiteral( "float" );
2085
        fieldSize = -1;
2086
        fieldPrec = 0;
2087
      }
2088
      else
2089
      {
2090
        fieldType = QStringLiteral( "decimal" );
2091
      }
2092
      break;
2093

    
2094
    default:
2095
      return false;
2096
  }
2097

    
2098
  field.setTypeName( fieldType );
2099
  field.setLength( fieldSize );
2100
  field.setPrecision( fieldPrec );
2101
  return true;
2102
}
2103

    
2104
void QgsMssqlProvider::mssqlWkbTypeAndDimension( Qgis::WkbType wkbType, QString &geometryType, int &dim )
2105
{
2106
  const Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
2107

    
2108
  if ( flatType == Qgis::WkbType::Point )
2109
    geometryType = QStringLiteral( "POINT" );
2110
  else if ( flatType == Qgis::WkbType::LineString )
2111
    geometryType = QStringLiteral( "LINESTRING" );
2112
  else if ( flatType == Qgis::WkbType::Polygon )
2113
    geometryType = QStringLiteral( "POLYGON" );
2114
  else if ( flatType == Qgis::WkbType::MultiPoint )
2115
    geometryType = QStringLiteral( "MULTIPOINT" );
2116
  else if ( flatType == Qgis::WkbType::MultiLineString )
2117
    geometryType = QStringLiteral( "MULTILINESTRING" );
2118
  else if ( flatType == Qgis::WkbType::MultiPolygon )
2119
    geometryType = QStringLiteral( "MULTIPOLYGON" );
2120
  else if ( flatType == Qgis::WkbType::GeometryCollection )
2121
    geometryType = QStringLiteral( "GEOMETRYCOLLECTION" );
2122
  else if ( flatType == Qgis::WkbType::CircularString )
2123
    geometryType = QStringLiteral( "CIRCULARSTRING" );
2124
  else if ( flatType == Qgis::WkbType::CompoundCurve )
2125
    geometryType = QStringLiteral( "COMPOUNDCURVE" );
2126
  else if ( flatType == Qgis::WkbType::CurvePolygon )
2127
    geometryType = QStringLiteral( "CURVEPOLYGON" );
2128
  else if ( flatType == Qgis::WkbType::Unknown )
2129
    geometryType = QStringLiteral( "GEOMETRY" );
2130
  else
2131
  {
2132
    dim = 0;
2133
    return;
2134
  }
2135

    
2136
  if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
2137
  {
2138
    dim = 4;
2139
  }
2140
  else if ( QgsWkbTypes::hasZ( wkbType ) )
2141
  {
2142
    dim = 3;
2143
  }
2144
  else if ( QgsWkbTypes::hasM( wkbType ) )
2145
  {
2146
    geometryType += QLatin1Char( 'M' );
2147
    dim = 3;
2148
  }
2149
  else if ( wkbType >= Qgis::WkbType::Point25D && wkbType <= Qgis::WkbType::MultiPolygon25D )
2150
  {
2151
    dim = 3;
2152
  }
2153
}
2154

    
2155
Qgis::WkbType QgsMssqlProvider::getWkbType( const QString &geometryType )
2156
{
2157
  return QgsWkbTypes::parseType( geometryType );
2158
}
2159

    
2160

    
2161
Qgis::VectorExportResult QgsMssqlProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> *oldToNewAttrIdxMap, QString *errorMessage, const QMap<QString, QVariant> *options )
2162
{
2163
  Q_UNUSED( options )
2164

    
2165
  // populate members from the uri structure
2166
  QgsDataSourceUri dsUri( uri );
2167

    
2168
  // connect to database
2169
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2170

    
2171
  if ( !db->isValid() )
2172
  {
2173
    if ( errorMessage )
2174
      *errorMessage = db->errorText();
2175
    return Qgis::VectorExportResult::ErrorConnectionFailed;
2176
  }
2177

    
2178
  const QString dbName = dsUri.database();
2179

    
2180
  QString schemaName = dsUri.schema();
2181
  const QString tableName = dsUri.table();
2182

    
2183
  QString geometryColumn = dsUri.geometryColumn();
2184

    
2185
  QString primaryKey = dsUri.keyColumn();
2186
  QString primaryKeyType;
2187

    
2188
  if ( schemaName.isEmpty() )
2189
    schemaName = QStringLiteral( "dbo" );
2190

    
2191
  if ( wkbType != Qgis::WkbType::NoGeometry && geometryColumn.isEmpty() )
2192
    geometryColumn = QStringLiteral( "geom" );
2193

    
2194
  // get the pk's name and type
2195
  bool createdNewPk = false;
2196

    
2197
  // if no pk name was passed, define the new pk field name
2198
  if ( primaryKey.isEmpty() )
2199
  {
2200
    int index = 0;
2201
    const QString pk = primaryKey = QStringLiteral( "qgs_fid" );
2202
    for ( int i = 0, n = fields.size(); i < n; ++i )
2203
    {
2204
      if ( fields.at( i ).name() == primaryKey )
2205
      {
2206
        // it already exists, try again with a new name
2207
        primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ );
2208
        i = 0;
2209
      }
2210
    }
2211
    createdNewPk = true;
2212
  }
2213
  else
2214
  {
2215
    // search for the passed field
2216
    for ( int i = 0, n = fields.size(); i < n; ++i )
2217
    {
2218
      if ( fields.at( i ).name() == primaryKey )
2219
      {
2220
        // found, get the field type
2221
        QgsField fld = fields.at( i );
2222
        if ( ( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) || convertField( fld ) )
2223
        {
2224
          primaryKeyType = fld.typeName();
2225
        }
2226
      }
2227
    }
2228
  }
2229

    
2230
  // if the field doesn't not exist yet, create it as a serial field
2231
  if ( primaryKeyType.isEmpty() )
2232
    primaryKeyType = QStringLiteral( "serial" );
2233

    
2234
  QString sql;
2235
  QSqlQuery q = QSqlQuery( db->db() );
2236
  q.setForwardOnly( true );
2237

    
2238
  // initialize metadata tables (same as OGR SQL)
2239
  sql = QString( "IF NOT EXISTS (SELECT * FROM sys.objects WHERE "
2240
                 "object_id = OBJECT_ID(N'[dbo].[geometry_columns]') AND type in (N'U')) "
2241
                 "CREATE TABLE geometry_columns (f_table_catalog varchar(128) not null, "
2242
                 "f_table_schema varchar(128) not null, f_table_name varchar(256) not null, "
2243
                 "f_geometry_column varchar(256) not null, coord_dimension integer not null, "
2244
                 "srid integer not null, geometry_type varchar(30) not null, "
2245
                 "CONSTRAINT geometry_columns_pk PRIMARY KEY (f_table_catalog, "
2246
                 "f_table_schema, f_table_name, f_geometry_column));\n"
2247
                 "IF NOT EXISTS (SELECT * FROM sys.objects "
2248
                 "WHERE object_id = OBJECT_ID(N'[dbo].[spatial_ref_sys]') AND type in (N'U')) "
2249
                 "CREATE TABLE spatial_ref_sys (srid integer not null "
2250
                 "PRIMARY KEY, auth_name varchar(256), auth_srid integer, srtext varchar(2048), proj4text varchar(2048))" );
2251

    
2252
  std::unique_ptr<QgsDatabaseQueryLogWrapper> logWrapper = std::make_unique<QgsDatabaseQueryLogWrapper>( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN );
2253

    
2254
  if ( !q.exec( sql ) )
2255
  {
2256
    logWrapper->setError( q.lastError().text() );
2257
    if ( errorMessage )
2258
      *errorMessage = q.lastError().text();
2259
    return Qgis::VectorExportResult::ErrorCreatingLayer;
2260
  }
2261

    
2262
  // set up spatial reference id
2263
  long srid = 0;
2264
  if ( srs.isValid() )
2265
  {
2266
    srid = srs.postgisSrid();
2267
    QString auth_srid = QStringLiteral( "null" );
2268
    QString auth_name = QStringLiteral( "null" );
2269
    QStringList sl = srs.authid().split( ':' );
2270
    if ( sl.length() == 2 )
2271
    {
2272
      auth_name = sl[0];
2273
      auth_srid = sl[1];
2274
    }
2275
    sql = QStringLiteral( "IF NOT EXISTS (SELECT * FROM spatial_ref_sys WHERE srid=%1) INSERT INTO spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text) VALUES (%1, %2, %3, %4, %5)" )
2276
            .arg( srid )
2277
            .arg( quotedValue( auth_name ), auth_srid, quotedValue( srs.toWkt() ), quotedValue( srs.toProj() ) );
2278

    
2279
    logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2280

    
2281
    if ( !q.exec( sql ) )
2282
    {
2283
      logWrapper->setError( q.lastError().text() );
2284
      if ( errorMessage )
2285
        *errorMessage = q.lastError().text();
2286
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2287
    }
2288
  }
2289

    
2290
  // get wkb type and dimension
2291
  QString geometryType;
2292
  int dim = 2;
2293
  mssqlWkbTypeAndDimension( wkbType, geometryType, dim );
2294

    
2295
  if ( overwrite )
2296
  {
2297
    // remove the old table with the same name
2298
    sql = QStringLiteral( "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')) BEGIN DROP TABLE [%1].[%2] DELETE FROM geometry_columns where f_table_schema='%1' and f_table_name='%2' END;" )
2299
            .arg( schemaName, tableName );
2300

    
2301
    logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2302

    
2303
    if ( !q.exec( sql ) )
2304
    {
2305
      logWrapper->setError( q.lastError().text() );
2306
      if ( errorMessage )
2307
        *errorMessage = q.lastError().text();
2308
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2309
    }
2310
  }
2311
  else
2312
  {
2313
    // test for existing
2314
    sql = QStringLiteral( "SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')" )
2315
            .arg( schemaName, tableName );
2316

    
2317
    logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2318

    
2319
    if ( !q.exec( sql ) )
2320
    {
2321
      logWrapper->setError( q.lastError().text() );
2322
      if ( errorMessage )
2323
        *errorMessage = q.lastError().text();
2324
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2325
    }
2326

    
2327
    // if we got a hit, abort!!
2328
    if ( q.next() )
2329
    {
2330
      if ( errorMessage )
2331
        *errorMessage = tr( "Table [%1].[%2] already exists" ).arg( schemaName, tableName );
2332
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2333
    }
2334
  }
2335

    
2336
  if ( !geometryColumn.isEmpty() )
2337
  {
2338
    sql = QStringLiteral( "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')) DROP TABLE [%1].[%2]\n"
2339
                          "CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL, [%4] [geometry] NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
2340
                          "DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
2341
                          "INSERT INTO [geometry_columns] ([f_table_catalog], [f_table_schema],[f_table_name], "
2342
                          "[f_geometry_column],[coord_dimension],[srid],[geometry_type]) VALUES ('%5', '%1', '%2', '%4', %6, %7, '%8')" )
2343
            .arg( schemaName, tableName, primaryKey, geometryColumn, dbName, QString::number( dim ), QString::number( srid ), geometryType );
2344
  }
2345
  else
2346
  {
2347
    //geometryless table
2348
    sql = QStringLiteral( "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')) DROP TABLE [%1].[%2]\n"
2349
                          "CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
2350
                          "DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
2351
    )
2352
            .arg( schemaName, tableName, primaryKey );
2353
  }
2354

    
2355
  logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
2356

    
2357
  if ( !q.exec( sql ) )
2358
  {
2359
    logWrapper->setError( q.lastError().text() );
2360
    if ( errorMessage )
2361
      *errorMessage = q.lastError().text();
2362
    return Qgis::VectorExportResult::ErrorCreatingLayer;
2363
  }
2364

    
2365
  // clear any resources hold by the query
2366
  q.clear();
2367
  q.setForwardOnly( true );
2368

    
2369
  // use the provider to edit the table
2370
  dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
2371

    
2372
  const QgsDataProvider::ProviderOptions providerOptions;
2373
  const Qgis::DataProviderReadFlags flags;
2374
  QgsMssqlProvider *provider = new QgsMssqlProvider( dsUri.uri(), providerOptions, flags );
2375
  if ( !provider->isValid() )
2376
  {
2377
    if ( errorMessage )
2378
      *errorMessage = QObject::tr( "Loading of the MSSQL provider failed" );
2379

    
2380
    delete provider;
2381
    return Qgis::VectorExportResult::ErrorInvalidLayer;
2382
  }
2383

    
2384
  // add fields to the layer
2385
  if ( oldToNewAttrIdxMap )
2386
    oldToNewAttrIdxMap->clear();
2387

    
2388
  if ( fields.size() > 0 )
2389
  {
2390
    // if we had to create a primary key column, we start the old columns from 1
2391
    int offset = createdNewPk ? 1 : 0;
2392

    
2393
    // get the list of fields
2394
    QList<QgsField> flist;
2395
    for ( int i = 0, n = fields.size(); i < n; ++i )
2396
    {
2397
      QgsField fld = fields.at( i );
2398
      if ( oldToNewAttrIdxMap && fld.name() == primaryKey )
2399
      {
2400
        oldToNewAttrIdxMap->insert( fields.lookupField( fld.name() ), 0 );
2401
        continue;
2402
      }
2403

    
2404
      if ( fld.name() == geometryColumn )
2405
      {
2406
        // Found a field with the same name of the geometry column. Skip it!
2407
        continue;
2408
      }
2409

    
2410
      if ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( fld ) )
2411
      {
2412
        if ( errorMessage )
2413
          *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( fld.name() );
2414

    
2415
        delete provider;
2416
        return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
2417
      }
2418

    
2419
      flist.append( fld );
2420
      if ( oldToNewAttrIdxMap )
2421
        oldToNewAttrIdxMap->insert( fields.lookupField( fld.name() ), offset++ );
2422
    }
2423

    
2424
    if ( !provider->addAttributes( flist ) )
2425
    {
2426
      if ( errorMessage )
2427
        *errorMessage = QObject::tr( "Creation of fields failed" );
2428

    
2429
      delete provider;
2430
      return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
2431
    }
2432
  }
2433
  return Qgis::VectorExportResult::Success;
2434
}
2435

    
2436

    
2437
/**
2438
 * Class factory to return a pointer to a newly created
2439
 * QgsMssqlProvider object
2440
 */
2441
QgsMssqlProvider *QgsMssqlProviderMetadata::createProvider(
2442
  const QString &uri,
2443
  const QgsDataProvider::ProviderOptions &options,
2444
  Qgis::DataProviderReadFlags flags
2445
)
2446
{
2447
  return new QgsMssqlProvider( uri, options, flags );
2448
}
2449

    
2450
QList<QgsDataItemProvider *> QgsMssqlProviderMetadata::dataItemProviders() const
2451
{
2452
  QList<QgsDataItemProvider *> providers;
2453
  providers << new QgsMssqlDataItemProvider;
2454
  return providers;
2455
}
2456

    
2457
QgsTransaction *QgsMssqlProviderMetadata::createTransaction( const QString &connString )
2458
{
2459
  return new QgsMssqlTransaction( connString );
2460
}
2461

    
2462
QMap<QString, QgsAbstractProviderConnection *> QgsMssqlProviderMetadata::connections( bool cached )
2463
{
2464
  return connectionsProtected<QgsMssqlProviderConnection, QgsMssqlConnection>( cached );
2465
}
2466

    
2467
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &name )
2468
{
2469
  return new QgsMssqlProviderConnection( name );
2470
}
2471

    
2472
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
2473
{
2474
  return new QgsMssqlProviderConnection( uri, configuration );
2475
}
2476

    
2477
void QgsMssqlProviderMetadata::deleteConnection( const QString &name )
2478
{
2479
  deleteConnectionProtected<QgsMssqlProviderConnection>( name );
2480
}
2481

    
2482
void QgsMssqlProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name )
2483
{
2484
  saveConnectionProtected( conn, name );
2485
}
2486

    
2487
Qgis::VectorExportResult QgsMssqlProviderMetadata::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> &oldToNewAttrIdxMap, QString &errorMessage, const QMap<QString, QVariant> *options )
2488
{
2489
  return QgsMssqlProvider::createEmptyLayer(
2490
    uri, fields, wkbType, srs, overwrite,
2491
    &oldToNewAttrIdxMap, &errorMessage, options
2492
  );
2493
}
2494

    
2495
bool QgsMssqlProviderMetadata::styleExists( const QString &uri, const QString &styleId, QString &errorCause )
2496
{
2497
  errorCause.clear();
2498
  const QgsDataSourceUri dsUri( uri );
2499
  // connect to database
2500
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2501

    
2502
  if ( !db->isValid() )
2503
  {
2504
    errorCause = QObject::tr( "Error connecting to database: %1" ).arg( db->errorText() );
2505
    return false;
2506
  }
2507

    
2508
  QSqlQuery query = QSqlQuery( db->db() );
2509
  query.setForwardOnly( true );
2510
  const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'layer_styles'" ) };
2511

    
2512
  if ( !LoggedExecMetadata( query, sql, uri ) )
2513
  {
2514
    errorCause = QObject::tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2515
    return false;
2516
  }
2517
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2518
  {
2519
    // no layer_styles table
2520
    query.finish();
2521
    return false;
2522
  }
2523

    
2524
  query.clear();
2525
  query.setForwardOnly( true );
2526
  const QString checkQuery = QString( "SELECT styleName"
2527
                                      " FROM layer_styles"
2528
                                      " WHERE f_table_catalog=%1"
2529
                                      " AND f_table_schema=%2"
2530
                                      " AND f_table_name=%3"
2531
                                      " AND f_geometry_column=%4"
2532
                                      " AND styleName=%5" )
2533
                               .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2534
                               .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2535
                               .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2536
                               .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) )
2537
                               .arg( QgsMssqlProvider::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) );
2538

    
2539
  if ( !LoggedExecMetadata( query, checkQuery, uri ) )
2540
  {
2541
    errorCause = QObject::tr( "Checking for style failed: %1" ).arg( query.lastError().text() );
2542
    return false;
2543
  }
2544

    
2545
  if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleId )
2546
  {
2547
    return true;
2548
  }
2549
  else
2550
  {
2551
    return false;
2552
  }
2553
}
2554

    
2555
bool QgsMssqlProviderMetadata::saveStyle( const QString &uri, const QString &qmlStyle, const QString &sldStyle, const QString &styleName, const QString &styleDescription, const QString &uiFileContent, bool useAsDefault, QString &errCause )
2556
{
2557
  const QgsDataSourceUri dsUri( uri );
2558
  // connect to database
2559
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2560

    
2561
  if ( !db->isValid() )
2562
  {
2563
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2564
    QgsDebugError( db->errorText() );
2565
    return false;
2566
  }
2567

    
2568
  QSqlQuery query = QSqlQuery( db->db() );
2569
  query.setForwardOnly( true );
2570

    
2571
  QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
2572

    
2573
  if ( !LoggedExecMetadata( query, sql, uri ) )
2574
  {
2575
    QgsDebugError( query.lastError().text() );
2576
    return false;
2577
  }
2578
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2579
  {
2580
    QgsDebugMsgLevel( QStringLiteral( "Need to create styles table" ), 2 );
2581

    
2582
    sql = QStringLiteral( "CREATE TABLE [dbo].[layer_styles]("
2583
                          "[id] int IDENTITY(1,1) PRIMARY KEY,"
2584
                          "[f_table_catalog] [varchar](1024) NULL,"
2585
                          "[f_table_schema] [varchar](1024) NULL,"
2586
                          "[f_table_name] [varchar](1024) NULL,"
2587
                          "[f_geometry_column] [varchar](1024) NULL,"
2588
                          "[styleName] [varchar](1024) NULL,"
2589
                          "[styleQML] [text] NULL,"
2590
                          "[styleSLD] [text] NULL,"
2591
                          "[useAsDefault] [int] NULL,"
2592
                          "[description] [text] NULL,"
2593
                          "[owner] [varchar](1024) NULL,"
2594
                          "[ui] [text] NULL,"
2595
                          "[update_time] [datetime] NULL"
2596
                          ") ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]" );
2597

    
2598
    const bool execOk = LoggedExecMetadata( query, sql, uri );
2599
    if ( !execOk )
2600
    {
2601
      const QString error { QObject::tr( "Unable to save layer style. It's not possible to create the destination table on the database. Maybe this is due to table permissions. Please contact your database admin" ) };
2602
      errCause = error;
2603
      return false;
2604
    }
2605
    query.finish();
2606
    query.clear();
2607
    query.setForwardOnly( true );
2608
  }
2609

    
2610
  QString uiFileColumn;
2611
  QString uiFileValue;
2612
  if ( !uiFileContent.isEmpty() )
2613
  {
2614
    uiFileColumn = QStringLiteral( ",ui" );
2615
    uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( uiFileContent );
2616
  }
2617
  QgsDebugMsgLevel( QStringLiteral( "Ready to insert new style" ), 2 );
2618
  // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values
2619
  // can contain user entered strings, which may themselves include %## values that would be
2620
  // replaced by the QString.arg function.  To ensure that the final SQL string is not corrupt these
2621
  // two values are both replaced in the final .arg call of the string construction.
2622

    
2623
  sql = QStringLiteral( "INSERT INTO layer_styles"
2624
                        "(f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner%11"
2625
                        ") VALUES ("
2626
                        "%1,%2,%3,%4,%5,%6,%7,%8,%9,%10%12"
2627
                        ")" )
2628
          .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2629
          .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2630
          .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2631
          .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) )
2632
          .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
2633
          .arg( QgsMssqlProvider::quotedValue( qmlStyle ) )
2634
          .arg( QgsMssqlProvider::quotedValue( sldStyle ) )
2635
          .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
2636
          .arg( QgsMssqlProvider::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
2637
          .arg( QgsMssqlProvider::quotedValue( dsUri.username() ) )
2638
          .arg( uiFileColumn )
2639
          .arg( uiFileValue );
2640

    
2641
  const QString checkQuery = QStringLiteral( "SELECT styleName"
2642
                                             " FROM layer_styles"
2643
                                             " WHERE f_table_catalog=%1"
2644
                                             " AND f_table_schema=%2"
2645
                                             " AND f_table_name=%3"
2646
                                             " AND f_geometry_column=%4"
2647
                                             " AND styleName=%5" )
2648
                               .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2649
                               .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2650
                               .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2651
                               .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) )
2652
                               .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
2653

    
2654
  if ( !LoggedExecMetadata( query, checkQuery, uri ) )
2655
  {
2656
    QgsDebugError( query.lastError().text() );
2657
    QgsDebugError( QStringLiteral( "Check Query failed" ) );
2658
    return false;
2659
  }
2660
  if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleName )
2661
  {
2662
    QgsDebugMsgLevel( QStringLiteral( "Updating styles" ), 2 );
2663
    sql = QString( "UPDATE layer_styles "
2664
                   " SET useAsDefault=%1"
2665
                   ",styleQML=%2"
2666
                   ",styleSLD=%3"
2667
                   ",description=%4"
2668
                   ",owner=%5"
2669
                   " WHERE f_table_catalog=%6"
2670
                   " AND f_table_schema=%7"
2671
                   " AND f_table_name=%8"
2672
                   " AND f_geometry_column=%9"
2673
                   " AND styleName=%10" )
2674
            .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
2675
            .arg( QgsMssqlProvider::quotedValue( qmlStyle ) )
2676
            .arg( QgsMssqlProvider::quotedValue( sldStyle ) )
2677
            .arg( QgsMssqlProvider::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
2678
            .arg( QgsMssqlProvider::quotedValue( dsUri.username() ) )
2679
            .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2680
            .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2681
            .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2682
            .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) )
2683
            .arg( QgsMssqlProvider::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
2684
  }
2685
  if ( useAsDefault )
2686
  {
2687
    const QString removeDefaultSql = QString( "UPDATE layer_styles "
2688
                                              " SET useAsDefault=0"
2689
                                              " WHERE f_table_catalog=%1"
2690
                                              " AND f_table_schema=%2"
2691
                                              " AND f_table_name=%3"
2692
                                              " AND f_geometry_column=%4" )
2693
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2694
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2695
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2696
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) );
2697
    sql = QStringLiteral( "%1; %2;" ).arg( removeDefaultSql, sql );
2698
  }
2699

    
2700
  QgsDebugMsgLevel( QStringLiteral( "Inserting styles" ), 2 );
2701
  QgsDebugMsgLevel( sql, 2 );
2702

    
2703
  const bool execOk = LoggedExecMetadata( query, sql, uri );
2704

    
2705
  if ( !execOk )
2706
  {
2707
    errCause = QObject::tr( "Unable to save layer style. It's not possible to insert a new record into the style table. Maybe this is due to table permissions. Please contact your database administrator." );
2708
  }
2709
  return execOk;
2710
}
2711

    
2712
QString QgsMssqlProviderMetadata::loadStyle( const QString &uri, QString &errCause )
2713
{
2714
  QString styleName;
2715
  return loadStoredStyle( uri, styleName, errCause );
2716
}
2717

    
2718
QString QgsMssqlProviderMetadata::loadStoredStyle( const QString &uri, QString &styleName, QString &errCause )
2719
{
2720
  errCause.clear();
2721
  const QgsDataSourceUri dsUri( uri );
2722
  // connect to database
2723
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2724

    
2725
  if ( !db->isValid() )
2726
  {
2727
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2728
    QgsDebugError( db->errorText() );
2729
    errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
2730
    return QString();
2731
  }
2732

    
2733
  QSqlQuery query = QSqlQuery( db->db() );
2734

    
2735
  query.setForwardOnly( true );
2736

    
2737
  const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
2738

    
2739
  if ( !LoggedExecMetadata( query, sql, uri ) )
2740
  {
2741
    errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2742
    return QString();
2743
  }
2744
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2745
  {
2746
    // no layer_styles table
2747
    errCause = tr( "Style does not exist" );
2748
    query.finish();
2749
    return QString();
2750
  }
2751

    
2752
  query.clear();
2753
  query.setForwardOnly( true );
2754

    
2755
  const QString selectQmlQuery = QString( "SELECT top 1 styleName, styleQML"
2756
                                          " FROM layer_styles"
2757
                                          " WHERE f_table_catalog=%1"
2758
                                          " AND f_table_schema=%2"
2759
                                          " AND f_table_name=%3"
2760
                                          " AND f_geometry_column=%4"
2761
                                          " ORDER BY useAsDefault desc" )
2762
                                   .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2763
                                   .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2764
                                   .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2765
                                   .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) );
2766

    
2767
  if ( !LoggedExecMetadata( query, selectQmlQuery, uri ) )
2768
  {
2769
    QgsDebugMsgLevel( QStringLiteral( "Load of style failed" ), 2 );
2770
    const QString msg = query.lastError().text();
2771
    errCause = msg;
2772
    QgsDebugError( msg );
2773
    return QString();
2774
  }
2775
  if ( query.isActive() && query.next() )
2776
  {
2777
    styleName = query.value( 0 ).toString();
2778
    const QString style = query.value( 1 ).toString();
2779
    return style;
2780
  }
2781
  return QString();
2782
}
2783

    
2784
int QgsMssqlProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause )
2785
{
2786
  const QgsDataSourceUri dsUri( uri );
2787
  // connect to database
2788
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2789

    
2790
  if ( !db->isValid() )
2791
  {
2792
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2793
    QgsDebugError( db->errorText() );
2794
    return -1;
2795
  }
2796

    
2797
  QSqlQuery query = QSqlQuery( db->db() );
2798
  query.setForwardOnly( true );
2799

    
2800
  QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
2801

    
2802
  // check if layer_styles table already exist
2803
  if ( !LoggedExecMetadata( query, sql, uri ) )
2804
  {
2805
    const QString msg = query.lastError().text();
2806
    errCause = msg;
2807
    QgsDebugError( msg );
2808
    return -1;
2809
  }
2810
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2811
  {
2812
    QgsDebugMsgLevel( QStringLiteral( "No styles available on DB" ), 2 );
2813
    return -1;
2814
  }
2815

    
2816
  const QString selectRelatedQuery = QString( "SELECT id,styleName,description"
2817
                                              " FROM layer_styles "
2818
                                              " WHERE f_table_catalog=%1"
2819
                                              " AND f_table_schema=%2"
2820
                                              " AND f_table_name=%3"
2821
                                              " AND f_geometry_column=%4"
2822
                                              " ORDER BY useasdefault DESC, update_time DESC" )
2823
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2824
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2825
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2826
                                       .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) );
2827

    
2828

    
2829
  bool queryOk = LoggedExecMetadata( query, selectRelatedQuery, uri );
2830
  if ( !queryOk )
2831
  {
2832
    QgsDebugError( query.lastError().text() );
2833
    return -1;
2834
  }
2835
  int numberOfRelatedStyles = 0;
2836
  while ( query.isActive() && query.next() )
2837
  {
2838
    QgsDebugMsgLevel( query.value( 1 ).toString(), 2 );
2839
    ids.append( query.value( 0 ).toString() );
2840
    names.append( query.value( 1 ).toString() );
2841
    descriptions.append( query.value( 2 ).toString() );
2842
    numberOfRelatedStyles = numberOfRelatedStyles + 1;
2843
  }
2844
  const QString selectOthersQuery = QString( "SELECT id,styleName,description"
2845
                                             " FROM layer_styles "
2846
                                             " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)"
2847
                                             " ORDER BY update_time DESC" )
2848
                                      .arg( QgsMssqlProvider::quotedValue( dsUri.database() ) )
2849
                                      .arg( QgsMssqlProvider::quotedValue( dsUri.schema() ) )
2850
                                      .arg( QgsMssqlProvider::quotedValue( dsUri.table() ) )
2851
                                      .arg( QgsMssqlProvider::quotedValue( dsUri.geometryColumn() ) );
2852
  QgsDebugMsgLevel( selectOthersQuery, 2 );
2853

    
2854
  queryOk = LoggedExecMetadata( query, selectOthersQuery, uri );
2855
  if ( !queryOk )
2856
  {
2857
    QgsDebugError( query.lastError().text() );
2858
    return -1;
2859
  }
2860
  while ( query.next() )
2861
  {
2862
    ids.append( query.value( 0 ).toString() );
2863
    names.append( query.value( 1 ).toString() );
2864
    descriptions.append( query.value( 2 ).toString() );
2865
  }
2866
  return numberOfRelatedStyles;
2867
}
2868

    
2869
QgsMssqlProviderMetadata::QgsMssqlProviderMetadata()
2870
  : QgsProviderMetadata( QgsMssqlProvider::MSSQL_PROVIDER_KEY, QgsMssqlProvider::MSSQL_PROVIDER_DESCRIPTION )
2871
{
2872
}
2873

    
2874
QIcon QgsMssqlProviderMetadata::icon() const
2875
{
2876
  return QgsApplication::getThemeIcon( QStringLiteral( "mIconMssql.svg" ) );
2877
}
2878

    
2879
QString QgsMssqlProviderMetadata::getStyleById( const QString &uri, const QString &styleId, QString &errCause )
2880
{
2881
  const QgsDataSourceUri dsUri( uri );
2882
  // connect to database
2883
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2884

    
2885
  if ( !db->isValid() )
2886
  {
2887
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2888
    QgsDebugError( db->errorText() );
2889
    errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
2890
    return QString();
2891
  }
2892

    
2893
  QSqlQuery query = QSqlQuery( db->db() );
2894
  query.setForwardOnly( true );
2895

    
2896
  const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
2897

    
2898
  if ( !LoggedExecMetadata( query, sql, uri ) )
2899
  {
2900
    errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2901
    return QString();
2902
  }
2903
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2904
  {
2905
    // no layer_styles table
2906
    errCause = tr( "Style does not exist" );
2907
    query.finish();
2908
    return QString();
2909
  }
2910

    
2911
  query.clear();
2912
  query.setForwardOnly( true );
2913

    
2914
  QString style;
2915
  const QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsMssqlProvider::quotedValue( styleId ) );
2916

    
2917
  const bool queryOk = LoggedExecMetadata( query, selectQmlQuery, uri );
2918
  if ( !queryOk )
2919
  {
2920
    QgsDebugError( query.lastError().text() );
2921
    errCause = query.lastError().text();
2922
    return QString();
2923
  }
2924
  if ( !query.next() )
2925
  {
2926
    errCause = tr( "Style does not exist" );
2927
  }
2928
  else
2929
  {
2930
    style = query.value( 0 ).toString();
2931
  }
2932
  return style;
2933
}
2934

    
2935
QVariantMap QgsMssqlProviderMetadata::decodeUri( const QString &uri ) const
2936
{
2937
  const QgsDataSourceUri dsUri { uri };
2938
  QVariantMap uriParts;
2939

    
2940
  if ( !dsUri.database().isEmpty() )
2941
    uriParts[QStringLiteral( "dbname" )] = dsUri.database();
2942
  if ( !dsUri.host().isEmpty() )
2943
    uriParts[QStringLiteral( "host" )] = dsUri.host();
2944
  if ( !dsUri.port().isEmpty() )
2945
    uriParts[QStringLiteral( "port" )] = dsUri.port();
2946
  if ( !dsUri.service().isEmpty() )
2947
    uriParts[QStringLiteral( "service" )] = dsUri.service();
2948
  if ( !dsUri.username().isEmpty() )
2949
    uriParts[QStringLiteral( "username" )] = dsUri.username();
2950
  if ( !dsUri.password().isEmpty() )
2951
    uriParts[QStringLiteral( "password" )] = dsUri.password();
2952

    
2953
  // Supported?
2954
  //if ( ! dsUri.authConfigId().isEmpty() )
2955
  //  uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId();
2956

    
2957
  if ( dsUri.wkbType() != Qgis::WkbType::Unknown )
2958
    uriParts[QStringLiteral( "type" )] = static_cast<quint32>( dsUri.wkbType() );
2959

    
2960
  // Supported?
2961
  // uriParts[ QStringLiteral( "selectatid" ) ] = dsUri.selectAtIdDisabled();
2962

    
2963
  if ( !dsUri.table().isEmpty() )
2964
    uriParts[QStringLiteral( "table" )] = dsUri.table();
2965
  if ( !dsUri.schema().isEmpty() )
2966
    uriParts[QStringLiteral( "schema" )] = dsUri.schema();
2967
  if ( !dsUri.keyColumn().isEmpty() )
2968
    uriParts[QStringLiteral( "key" )] = dsUri.keyColumn();
2969
  if ( !dsUri.srid().isEmpty() )
2970
    uriParts[QStringLiteral( "srid" )] = dsUri.srid();
2971

    
2972
  uriParts[QStringLiteral( "estimatedmetadata" )] = dsUri.useEstimatedMetadata();
2973

    
2974
  // is this supported?
2975
  // uriParts[ QStringLiteral( "sslmode" ) ] = dsUri.sslMode();
2976

    
2977
  if ( !dsUri.sql().isEmpty() )
2978
    uriParts[QStringLiteral( "sql" )] = dsUri.sql();
2979
  if ( !dsUri.geometryColumn().isEmpty() )
2980
    uriParts[QStringLiteral( "geometrycolumn" )] = dsUri.geometryColumn();
2981

    
2982
  // From configuration
2983
  static const QStringList configurationParameters {
2984
    QStringLiteral( "geometryColumnsOnly" ),
2985
    QStringLiteral( "allowGeometrylessTables" ),
2986
    QStringLiteral( "saveUsername" ),
2987
    QStringLiteral( "savePassword" ),
2988
    QStringLiteral( "estimatedMetadata" ),
2989
    QStringLiteral( "disableInvalidGeometryHandling" ),
2990
    QStringLiteral( "extentInGeometryColumns" ),
2991
    QStringLiteral( "primaryKeyInGeometryColumns" )
2992
  };
2993

    
2994
  for ( const auto &configParam : configurationParameters )
2995
  {
2996
    if ( dsUri.hasParam( configParam ) )
2997
    {
2998
      uriParts[configParam] = dsUri.param( configParam );
2999
    }
3000
  }
3001

    
3002
  return uriParts;
3003
}
3004

    
3005
QString QgsMssqlProviderMetadata::encodeUri( const QVariantMap &parts ) const
3006
{
3007
  QgsDataSourceUri dsUri;
3008
  if ( parts.contains( QStringLiteral( "dbname" ) ) )
3009
    dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
3010
  // Also accepts "database"
3011
  if ( parts.contains( QStringLiteral( "database" ) ) )
3012
    dsUri.setDatabase( parts.value( QStringLiteral( "database" ) ).toString() );
3013
  // Supported?
3014
  //if ( parts.contains( QStringLiteral( "port" ) ) )
3015
  //  dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
3016
  if ( parts.contains( QStringLiteral( "host" ) ) )
3017
    dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
3018
  if ( parts.contains( QStringLiteral( "service" ) ) )
3019
    dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
3020
  if ( parts.contains( QStringLiteral( "username" ) ) )
3021
    dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
3022
  if ( parts.contains( QStringLiteral( "password" ) ) )
3023
    dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
3024
  // Supported?
3025
  //if ( parts.contains( QStringLiteral( "authcfg" ) ) )
3026
  //  dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
3027
  if ( parts.contains( QStringLiteral( "type" ) ) )
3028
    dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<Qgis::WkbType>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
3029
  // Supported?
3030
  //if ( parts.contains( QStringLiteral( "selectatid" ) ) )
3031
  //  dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
3032
  if ( parts.contains( QStringLiteral( "table" ) ) )
3033
    dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
3034
  if ( parts.contains( QStringLiteral( "schema" ) ) )
3035
    dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
3036
  if ( parts.contains( QStringLiteral( "key" ) ) )
3037
    dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
3038
  if ( parts.contains( QStringLiteral( "srid" ) ) )
3039
    dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
3040
  if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
3041
    dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
3042
  // Supported?
3043
  //if ( parts.contains( QStringLiteral( "sslmode" ) ) )
3044
  //  dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt( ) ) ) );
3045
  if ( parts.contains( QStringLiteral( "sql" ) ) )
3046
    dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
3047
  // Supported?
3048
  //if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
3049
  //  dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
3050
  if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
3051
    dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
3052
  if ( parts.contains( QStringLiteral( "disableInvalidGeometryHandling" ) ) )
3053
    dsUri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), parts.value( QStringLiteral( "disableInvalidGeometryHandling" ) ).toString() );
3054
  if ( parts.contains( QStringLiteral( "allowGeometrylessTables" ) ) )
3055
    dsUri.setParam( QStringLiteral( "allowGeometrylessTables" ), parts.value( QStringLiteral( "allowGeometrylessTables" ) ).toString() );
3056
  if ( parts.contains( QStringLiteral( "geometryColumnsOnly" ) ) )
3057
    dsUri.setParam( QStringLiteral( "geometryColumnsOnly" ), parts.value( QStringLiteral( "geometryColumnsOnly" ) ).toString() );
3058
  if ( parts.contains( QStringLiteral( "extentInGeometryColumns" ) ) )
3059
    dsUri.setParam( QStringLiteral( "extentInGeometryColumns" ), parts.value( QStringLiteral( "extentInGeometryColumns" ) ).toString() );
3060
  if ( parts.contains( QStringLiteral( "primaryKeyInGeometryColumns" ) ) )
3061
    dsUri.setParam( QStringLiteral( "primaryKeyInGeometryColumns" ), parts.value( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toString() );
3062
  return dsUri.uri();
3063
}
3064

    
3065
QList<Qgis::LayerType> QgsMssqlProviderMetadata::supportedLayerTypes() const
3066
{
3067
  return { Qgis::LayerType::Vector };
3068
}
3069

    
3070
QString QgsMssqlProvider::typeFromMetadata( const QString &typeName, int numCoords )
3071
{
3072
  QString type { typeName };
3073
  const bool hasM { typeName.endsWith( 'M', Qt::CaseInsensitive ) };
3074
  if ( numCoords == 4 )
3075
  {
3076
    if ( hasM )
3077
    {
3078
      type.chop( 1 );
3079
    }
3080
    type.append( QStringLiteral( "ZM" ) );
3081
  }
3082
  else if ( numCoords == 3 )
3083
  {
3084
    if ( !hasM )
3085
    {
3086
      type.append( QStringLiteral( "Z" ) );
3087
    }
3088
  }
3089
  return type;
3090
}
3091

    
3092
bool QgsMssqlProviderMetadata::execLogged( QSqlQuery &qry, const QString &sql, const QString &uri, const QString &queryOrigin ) const
3093
{
3094
  QgsDatabaseQueryLogWrapper logWrapper { sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProviderMetadata" ), queryOrigin };
3095
  const bool res { qry.exec( sql ) };
3096
  if ( !res )
3097
  {
3098
    logWrapper.setError( qry.lastError().text() );
3099
  }
3100
  else
3101
  {
3102
    if ( qry.isSelect() )
3103
    {
3104
      logWrapper.setFetchedRows( qry.size() );
3105
    }
3106
    else
3107
    {
3108
      logWrapper.setFetchedRows( qry.numRowsAffected() );
3109
    }
3110
  }
3111
  logWrapper.setQuery( qry.lastQuery() );
3112
  return res;
3113
}
3114

    
3115
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
3116
{
3117
  return new QgsMssqlProviderMetadata();
3118
}
3119

    
3120
// ----------
3121

    
3122
QgsFeatureId QgsMssqlSharedData::lookupFid( const QVariantList &v )
3123
{
3124
  const QMutexLocker locker( &mMutex );
3125

    
3126
  const QMap<QVariantList, QgsFeatureId>::const_iterator it = mKeyToFid.constFind( v );
3127

    
3128
  if ( it != mKeyToFid.constEnd() )
3129
  {
3130
    return it.value();
3131
  }
3132

    
3133
  mFidToKey.insert( ++mFidCounter, v );
3134
  mKeyToFid.insert( v, mFidCounter );
3135

    
3136
  return mFidCounter;
3137
}
3138

    
3139
QVariant QgsMssqlSharedData::removeFid( QgsFeatureId fid )
3140
{
3141
  const QMutexLocker locker( &mMutex );
3142

    
3143
  const QVariantList v = mFidToKey[fid];
3144
  mFidToKey.remove( fid );
3145
  mKeyToFid.remove( v );
3146
  return v;
3147
}
3148

    
3149
void QgsMssqlSharedData::insertFid( QgsFeatureId fid, const QVariantList &k )
3150
{
3151
  const QMutexLocker locker( &mMutex );
3152

    
3153
  mFidToKey.insert( fid, k );
3154
  mKeyToFid.insert( k, fid );
3155
}
3156

    
3157
QVariantList QgsMssqlSharedData::lookupKey( QgsFeatureId featureId )
3158
{
3159
  const QMutexLocker locker( &mMutex );
3160

    
3161
  const QMap<QgsFeatureId, QVariantList>::const_iterator it = mFidToKey.find( featureId );
3162
  if ( it != mFidToKey.constEnd() )
3163
    return it.value();
3164
  return QVariantList();
3165
}
3166

    
3167
QString QgsMssqlProvider::whereClauseFid( QgsFeatureId featureId )
3168
{
3169
  QString whereClause;
3170

    
3171
  switch ( mPrimaryKeyType )
3172
  {
3173
    case PktInt:
3174
      Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
3175
      whereClause = QStringLiteral( "[%1]=%2" ).arg( mAttributeFields.at( mPrimaryKeyAttrs[0] ).name(), FID_TO_STRING( featureId ) );
3176
      break;
3177

    
3178
    case PktFidMap:
3179
    {
3180
      const QVariantList &pkVals = mShared->lookupKey( featureId );
3181
      if ( !pkVals.isEmpty() )
3182
      {
3183
        Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() );
3184

    
3185
        whereClause = QStringLiteral( "(" );
3186

    
3187
        QString delim;
3188
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
3189
        {
3190
          const QgsField &fld = mAttributeFields.at( mPrimaryKeyAttrs[i] );
3191
          whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), quotedValue( pkVals[i] ) );
3192
          delim = QStringLiteral( " AND " );
3193
        }
3194

    
3195
        whereClause += QLatin1Char( ')' );
3196
      }
3197
      else
3198
      {
3199
        QgsDebugError( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) );
3200
        whereClause = QStringLiteral( "NULL IS NOT NULL" );
3201
      }
3202
    }
3203
    break;
3204

    
3205
    default:
3206
      Q_ASSERT( !"FAILURE: Primary key unknown" );
3207
      whereClause = QStringLiteral( "NULL IS NOT NULL" );
3208
      break;
3209
  }
3210

    
3211
  return whereClause;
3212
}
3213

    
3214
/* static */
3215
QStringList QgsMssqlProvider::parseUriKey( const QString &key )
3216
{
3217
  if ( key.isEmpty() )
3218
    return QStringList();
3219

    
3220
  QStringList cols;
3221

    
3222
  // remove quotes from key list
3223
  if ( key.startsWith( '"' ) && key.endsWith( '"' ) )
3224
  {
3225
    int i = 1;
3226
    QString col;
3227
    while ( i < key.size() )
3228
    {
3229
      if ( key[i] == '"' )
3230
      {
3231
        if ( i + 1 < key.size() && key[i + 1] == '"' )
3232
        {
3233
          i++;
3234
        }
3235
        else
3236
        {
3237
          cols << col;
3238
          col.clear();
3239

    
3240
          if ( ++i == key.size() )
3241
            break;
3242

    
3243
          Q_ASSERT( key[i] == ',' );
3244
          i++;
3245
          Q_ASSERT( key[i] == '"' );
3246
          i++;
3247
          col.clear();
3248
          continue;
3249
        }
3250
      }
3251

    
3252
      col += key[i++];
3253
    }
3254
  }
3255
  else if ( key.contains( ',' ) )
3256
  {
3257
    cols = key.split( ',' );
3258
  }
3259
  else
3260
  {
3261
    cols << key;
3262
  }
3263

    
3264
  return cols;
3265
}
3266

    
3267
bool QgsMssqlProvider::getExtentFromGeometryColumns( QgsRectangle &extent ) const
3268
{
3269
  QSqlQuery query = createQuery();
3270
  query.setForwardOnly( true );
3271

    
3272
  const QString sql = QStringLiteral( "SELECT qgis_xmin,qgis_xmax,qgis_ymin,qgis_ymax "
3273
                                      "FROM geometry_columns WHERE f_table_name = %1 AND f_table_schema = %2 "
3274
                                      "AND NOT (qgis_xmin IS NULL OR qgis_xmax IS NULL OR qgis_ymin IS NULL OR qgis_ymax IS NULL)" );
3275

    
3276
  const QString statement = sql.arg( quotedValue( mTableName ), quotedValue( mSchemaName ) );
3277

    
3278
  if ( LoggedExec( query, statement ) && query.isActive() )
3279
  {
3280
    query.next();
3281
    if ( query.isValid() )
3282
    {
3283
      extent.setXMinimum( query.value( 0 ).toDouble() );
3284
      extent.setXMaximum( query.value( 1 ).toDouble() );
3285
      extent.setYMinimum( query.value( 2 ).toDouble() );
3286
      extent.setYMaximum( query.value( 3 ).toDouble() );
3287

    
3288
      return true;
3289
    }
3290
  }
3291

    
3292
  return false;
3293
}
3294

    
3295
bool QgsMssqlProvider::getPrimaryKeyFromGeometryColumns( QStringList &primaryKeys )
3296
{
3297
  QSqlQuery query = createQuery();
3298
  query.setForwardOnly( true );
3299
  primaryKeys.clear();
3300

    
3301
  const QString sql = QStringLiteral( "SELECT qgis_pkey FROM geometry_columns "
3302
                                      "WHERE f_table_name = %1 AND f_table_schema = %2 AND NOT qgis_pkey IS NULL" );
3303
  const QString statement = sql.arg( quotedValue( mTableName ), quotedValue( mSchemaName ) );
3304

    
3305
  if ( LoggedExec( query, statement ) && query.isActive() )
3306
  {
3307
    query.next();
3308
    if ( query.isValid() )
3309
    {
3310
      primaryKeys = query.value( 0 ).toString().split( ',' );
3311
      if ( !primaryKeys.isEmpty() )
3312
        return true;
3313
    }
3314
  }
3315

    
3316
  return false;
3317
}