Statistics
| Branch: | Tag: | Revision:

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

History | View | Annotate | Download (104 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 "qgsmssqlutils.h"
24
#include "qgsfeedback.h"
25
#include "qgsdbquerylog.h"
26
#include "qgsdbquerylog_p.h"
27
#include "qgsvariantutils.h"
28
#include "qgsthreadingutils.h"
29

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

    
45

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

    
56
#include "qgsmssqldataitems.h"
57
#include "qgsmssqlfeatureiterator.h"
58
#include "qgsmssqltransaction.h"
59

    
60

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

    
67

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

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

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

    
83
  mWkbType = anUri.wkbType();
84

    
85
  mValid = true;
86

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

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

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

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

    
107
  mSqlWhereClause = anUri.sql();
108

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

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

    
125
  // Database successfully opened; we can now issue SQL commands.
126

    
127
  if ( mSchemaName.isEmpty() && anUri.table().startsWith( '(' ) && anUri.table().endsWith( ')' ) )
128
  {
129
    mIsQuery = true;
130
    mQuery = anUri.table();
131
  }
132
  else
133
  {
134
    mIsQuery = false;
135
    if ( !anUri.schema().isEmpty() )
136
      mSchemaName = anUri.schema();
137
    else
138
      mSchemaName = QStringLiteral( "dbo" );
139

    
140
    if ( !anUri.table().isEmpty() )
141
    {
142
      // the layer name has been specified
143
      mTableName = anUri.table();
144
      QStringList sl = mTableName.split( '.' );
145
      if ( sl.length() == 2 )
146
      {
147
        mSchemaName = sl[0];
148
        mTableName = sl[1];
149
      }
150
      mTables = QStringList( mTableName );
151
    }
152
    else
153
    {
154
      // Get a list of table
155
      mTables = db.tables( QSql::Tables );
156
      if ( !mTables.isEmpty() )
157
        mTableName = mTables[0];
158
      else
159
        mValid = false;
160
    }
161
  }
162

    
163
  if ( mValid )
164
  {
165
    if ( !anUri.geometryColumn().isEmpty() )
166
      mGeometryColName = anUri.geometryColumn();
167

    
168
    if ( !mIsQuery )
169
    {
170
      if ( mSRId < 0 || mWkbType == Qgis::WkbType::Unknown || mGeometryColName.isEmpty() )
171
      {
172
        loadMetadata();
173
      }
174
      else
175
      {
176
        // TODO query??
177
      }
178
    }
179
    loadFields();
180

    
181
    UpdateStatistics( mUseEstimatedMetadata );
182

    
183
    //only for views, defined in layer data when loading layer for first time
184
    bool primaryKeyFromGeometryColumnsTable = anUri.hasParam( QStringLiteral( "primaryKeyInGeometryColumns" ) )
185
                                                ? anUri.param( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toInt()
186
                                                : false;
187

    
188
    QStringList cols;
189
    if ( primaryKeyFromGeometryColumnsTable )
190
    {
191
      mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Unknown;
192
      mPrimaryKeyAttrs.clear();
193
      primaryKeyFromGeometryColumnsTable = getPrimaryKeyFromGeometryColumns( cols );
194
      if ( !primaryKeyFromGeometryColumnsTable )
195
        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" ) );
196
    }
197

    
198
    if ( !primaryKeyFromGeometryColumnsTable )
199
    {
200
      const QString primaryKey = anUri.keyColumn();
201
      if ( !primaryKey.isEmpty() )
202
      {
203
        mPrimaryKeyAttrs.clear();
204
        cols = parseUriKey( primaryKey );
205
      }
206
    }
207

    
208
    if ( mValid )
209
    {
210
      for ( const QString &col : std::as_const( cols ) )
211
      {
212
        const int idx = mAttributeFields.indexFromName( col );
213
        if ( idx < 0 )
214
        {
215
          QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "MSSQL" ) );
216
          mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Unknown;
217
          mPrimaryKeyAttrs.clear();
218
          break;
219
        }
220

    
221
        const QgsField &fld = mAttributeFields.at( idx );
222

    
223
        if ( mPrimaryKeyAttrs.size() == 0 && ( fld.type() == QMetaType::Type::Int || fld.type() == QMetaType::Type::LongLong || ( fld.type() == QMetaType::Type::Double && fld.precision() == 0 ) ) )
224
        {
225
          mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Int;
226
        }
227
        else
228
        {
229
          mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::FidMap;
230
        }
231

    
232
        mPrimaryKeyAttrs << idx;
233
      }
234

    
235
      if ( mGeometryColName.isEmpty() )
236
      {
237
        // table contains no geometries
238
        mWkbType = Qgis::WkbType::NoGeometry;
239
        mSRId = 0;
240
      }
241
    }
242
  }
243

    
244

    
245
  if ( mValid && mIsQuery && mPrimaryKeyAttrs.isEmpty() )
246
  {
247
    const QString error = QStringLiteral( "No primary key could be found for query %1" ).arg( mQuery );
248
    QgsDebugError( error );
249
    mValid = false;
250
    setLastError( error );
251
  }
252

    
253
  //fill type names into sets
254
  setNativeTypes( QgsMssqlConnection::nativeTypes() );
255
}
256

    
257
QgsMssqlProvider::~QgsMssqlProvider()
258
{
259
}
260

    
261
QgsAbstractFeatureSource *QgsMssqlProvider::featureSource() const
262
{
263
  return new QgsMssqlFeatureSource( this );
264
}
265

    
266
QgsFeatureIterator QgsMssqlProvider::getFeatures( const QgsFeatureRequest &request ) const
267
{
268
  if ( !mValid )
269
  {
270
    QgsDebugError( QStringLiteral( "Read attempt on an invalid mssql data source" ) );
271
    return QgsFeatureIterator();
272
  }
273

    
274
  return QgsFeatureIterator( new QgsMssqlFeatureIterator( new QgsMssqlFeatureSource( this ), true, request ) );
275
}
276

    
277
void QgsMssqlProvider::loadMetadata()
278
{
279
  mSRId = 0;
280
  mWkbType = Qgis::WkbType::Unknown;
281

    
282
  QSqlQuery query = createQuery();
283
  query.setForwardOnly( true );
284
  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( QgsMssqlUtils::quotedValue( mSchemaName ), QgsMssqlUtils::quotedValue( mTableName ) ) ) )
285
  {
286
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
287
  }
288

    
289
  if ( query.isActive() && query.next() )
290
  {
291
    mGeometryColName = query.value( 0 ).toString();
292
    mSRId = query.value( 1 ).toInt();
293
    const int dimensions = query.value( 3 ).toInt();
294
    const QString detectedType { QgsMssqlProvider::typeFromMetadata( query.value( 2 ).toString().toUpper(), dimensions ) };
295
    mWkbType = getWkbType( detectedType );
296
  }
297
}
298

    
299
bool QgsMssqlProvider::execLogged( QSqlQuery &qry, const QString &sql, const QString &queryOrigin ) const
300
{
301
  QgsDatabaseQueryLogWrapper logWrapper { sql, uri().uri(), QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), queryOrigin };
302
  const bool res { qry.exec( sql ) };
303
  if ( !res )
304
  {
305
    logWrapper.setError( qry.lastError().text() );
306
  }
307
  else
308
  {
309
    if ( qry.isSelect() )
310
    {
311
      logWrapper.setFetchedRows( qry.size() );
312
    }
313
    else
314
    {
315
      logWrapper.setFetchedRows( qry.numRowsAffected() );
316
    }
317
  }
318
  logWrapper.setQuery( qry.lastQuery() );
319
  return res;
320
}
321

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

    
345
void QgsMssqlProvider::setLastError( const QString &error )
346
{
347
  appendError( error );
348
  mLastError = error;
349
}
350

    
351
QSqlQuery QgsMssqlProvider::createQuery() const
352
{
353
  std::shared_ptr<QgsMssqlDatabase> conn = connection();
354
  return conn->createQuery();
355
}
356

    
357
void QgsMssqlProvider::loadFields()
358
{
359
  mAttributeFields.clear();
360
  mDefaultValues.clear();
361
  mComputedColumns.clear();
362

    
363
  std::shared_ptr<QgsMssqlDatabase> conn = connection();
364

    
365
  QgsMssqlDatabase::FieldDetails details;
366
  details.geometryColumnName = mGeometryColName;
367

    
368
  QString error;
369
  const bool result = mIsQuery ? conn->loadQueryFields( details, mQuery, error )
370
                               : conn->loadFields( details, mSchemaName, mTableName, error );
371
  if ( !result )
372
  {
373
    pushError( error );
374
    return;
375
  }
376

    
377
  mComputedColumns = details.computedColumns;
378
  mGeometryColName = details.geometryColumnName;
379
  mGeometryColType = details.geometryColumnType;
380
  mParser.mIsGeography = details.isGeography;
381
  mPrimaryKeyType = details.primaryKeyType;
382
  mPrimaryKeyAttrs = details.primaryKeyAttrs;
383
  mAttributeFields = details.attributeFields;
384
  mDefaultValues = details.defaultValues;
385

    
386
  if ( !mIsQuery && mPrimaryKeyAttrs.isEmpty() )
387
  {
388
    const QString error = QStringLiteral( "No primary key could be found on table %1" ).arg( mTableName );
389
    QgsDebugError( error );
390
    mValid = false;
391
    setLastError( error );
392
  }
393
}
394

    
395
QString QgsMssqlProvider::defaultValueClause( int fieldId ) const
396
{
397
  const QString defVal = mDefaultValues.value( fieldId, QString() );
398

    
399
  if ( defVal.isEmpty() )
400
    return QString();
401

    
402
  // NOTE: If EvaluateDefaultValues is activated it is impossible to get the defaultValueClause.
403
  //       This also apply to QgsPostgresProvider::defaultValueClause.
404
  if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
405
    return defVal;
406

    
407
  return QString();
408
}
409

    
410
QVariant QgsMssqlProvider::defaultValue( int fieldId ) const
411
{
412
  const QString defVal = mDefaultValues.value( fieldId, QString() );
413

    
414
  if ( defVal.isEmpty() )
415
    return QVariant();
416

    
417
  if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
418
    return QVariant();
419

    
420
  const QString sql = QStringLiteral( "select %1" )
421
                        .arg( defVal );
422

    
423
  QSqlQuery query = createQuery();
424
  query.setForwardOnly( true );
425

    
426
  if ( !LoggedExec( query, sql ) )
427
  {
428
    const QString errorMessage( tr( "Could not execute query: %1" ).arg( query.lastError().text() ) );
429
    QgsDebugError( errorMessage );
430
    pushError( errorMessage );
431
    return QVariant();
432
  }
433

    
434
  if ( !query.next() )
435
  {
436
    const QString errorMessage( tr( "Could not fetch next query value: %1" ).arg( query.lastError().text() ) );
437
    QgsDebugError( errorMessage );
438
    pushError( errorMessage );
439
    return QVariant();
440
  }
441

    
442
  const QVariant res = query.value( 0 );
443
  return QgsVariantUtils::isNull( res ) ? QVariant() : res;
444
}
445

    
446
QString QgsMssqlProvider::storageType() const
447
{
448
  return QStringLiteral( "MSSQL spatial database" );
449
}
450

    
451
QVariant QgsMssqlProvider::convertTimeValue( const QVariant &value )
452
{
453
  if ( value.isValid() && value.userType() == QMetaType::Type::QByteArray )
454
  {
455
    // time fields can be returned as byte arrays... woot
456
    const QByteArray ba = value.toByteArray();
457
    if ( ba.length() >= 5 )
458
    {
459
      const int hours = ba.at( 0 );
460
      const int mins = ba.at( 2 );
461
      const int seconds = ba.at( 4 );
462
      QVariant t = QTime( hours, mins, seconds );
463
      if ( !t.isValid() ) // can't handle it
464
        t = QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
465
      return t;
466
    }
467
    return QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
468
  }
469
  return value;
470
}
471

    
472
// Returns the minimum value of an attribute
473
QVariant QgsMssqlProvider::minimumValue( int index ) const
474
{
475
  if ( index < 0 || index >= mAttributeFields.count() )
476
  {
477
    return QVariant();
478
  }
479

    
480
  // get the field name
481
  const QgsField &fld = mAttributeFields.at( index );
482
  QString sql = QStringLiteral( "SELECT min(%1) FROM " )
483
                  .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
484

    
485
  if ( mIsQuery )
486
  {
487
    sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
488
  }
489
  else
490
  {
491
    sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
492
    if ( !mSqlWhereClause.isEmpty() )
493
    {
494
      sql += " WHERE (" + mSqlWhereClause + ')';
495
    }
496
  }
497

    
498
  QSqlQuery query = createQuery();
499
  query.setForwardOnly( true );
500

    
501
  if ( !LoggedExec( query, sql ) )
502
  {
503
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
504
  }
505

    
506
  if ( query.isActive() && query.next() )
507
  {
508
    QVariant v = query.value( 0 );
509
    if ( fld.type() == QMetaType::Type::QTime )
510
      v = convertTimeValue( v );
511
    if ( v.userType() != fld.type() )
512
      v = convertValue( fld.type(), v.toString() );
513
    return v;
514
  }
515

    
516
  return QVariant();
517
}
518

    
519
// Returns the maximum value of an attribute
520
QVariant QgsMssqlProvider::maximumValue( int index ) const
521
{
522
  if ( index < 0 || index >= mAttributeFields.count() )
523
  {
524
    return QVariant();
525
  }
526

    
527
  // get the field name
528
  const QgsField &fld = mAttributeFields.at( index );
529
  QString sql = QStringLiteral( "SELECT max(%1) FROM " )
530
                  .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
531

    
532
  if ( mIsQuery )
533
  {
534
    sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
535
  }
536
  else
537
  {
538
    sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
539
    if ( !mSqlWhereClause.isEmpty() )
540
    {
541
      sql += " WHERE (" + mSqlWhereClause + ')';
542
    }
543
  }
544

    
545
  QSqlQuery query = createQuery();
546
  query.setForwardOnly( true );
547

    
548
  if ( !LoggedExec( query, sql ) )
549
  {
550
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
551
  }
552

    
553
  if ( query.isActive() && query.next() )
554
  {
555
    QVariant v = query.value( 0 );
556
    if ( fld.type() == QMetaType::Type::QTime )
557
      v = convertTimeValue( v );
558
    if ( v.userType() != fld.type() )
559
      v = convertValue( fld.type(), v.toString() );
560
    return v;
561
  }
562

    
563
  return QVariant();
564
}
565

    
566
// Returns the list of unique values of an attribute
567
QSet<QVariant> QgsMssqlProvider::uniqueValues( int index, int limit ) const
568
{
569
  QSet<QVariant> uniqueValues;
570
  if ( index < 0 || index >= mAttributeFields.count() )
571
  {
572
    return uniqueValues;
573
  }
574

    
575
  // get the field name
576
  const QgsField &fld = mAttributeFields.at( index );
577
  QString sql = QStringLiteral( "SELECT DISTINCT " );
578

    
579
  if ( limit > 0 )
580
  {
581
    sql += QStringLiteral( " TOP %1 " ).arg( limit );
582
  }
583

    
584
  sql += QStringLiteral( "%1 FROM " ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
585

    
586

    
587
  if ( mIsQuery )
588
  {
589
    sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
590
  }
591
  else
592
  {
593
    sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
594
    if ( !mSqlWhereClause.isEmpty() )
595
    {
596
      sql += " where (" + mSqlWhereClause + ')';
597
    }
598
  }
599

    
600
  QSqlQuery query = createQuery();
601
  query.setForwardOnly( true );
602

    
603
  if ( !LoggedExec( query, sql ) )
604
  {
605
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
606
  }
607

    
608
  if ( query.isActive() )
609
  {
610
    // read all features
611
    while ( query.next() )
612
    {
613
      QVariant v = query.value( 0 );
614
      if ( fld.type() == QMetaType::Type::QTime )
615
        v = convertTimeValue( v );
616
      if ( v.userType() != fld.type() )
617
        v = convertValue( fld.type(), v.toString() );
618
      uniqueValues.insert( v );
619
    }
620
  }
621
  return uniqueValues;
622
}
623

    
624
QStringList QgsMssqlProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
625
{
626
  QStringList results;
627

    
628
  if ( index < 0 || index >= mAttributeFields.count() )
629
  {
630
    return results;
631
  }
632

    
633
  // get the field name
634
  const QgsField &fld = mAttributeFields.at( index );
635
  QString sql = QStringLiteral( "SELECT DISTINCT " );
636

    
637
  if ( limit > 0 )
638
  {
639
    sql += QStringLiteral( " TOP %1 " ).arg( limit );
640
  }
641

    
642
  sql += QStringLiteral( "%1 FROM " )
643
           .arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
644

    
645
  if ( mIsQuery )
646
  {
647
    sql += QStringLiteral( " (%1) q WHERE %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause ) : QString() );
648
  }
649
  else
650
  {
651
    sql += QStringLiteral( " %1.%2 WHERE " ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
652
    if ( !mSqlWhereClause.isEmpty() )
653
    {
654
      sql += QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause );
655
    }
656
  }
657

    
658
  sql += QStringLiteral( " %1 LIKE '%%2%'" ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ), substring );
659

    
660
  QSqlQuery query = createQuery();
661
  query.setForwardOnly( true );
662

    
663
  if ( !LoggedExec( query, sql ) )
664
  {
665
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
666
  }
667

    
668
  if ( query.isActive() )
669
  {
670
    // read all features
671
    while ( query.next() )
672
    {
673
      results << query.value( 0 ).toString();
674
      if ( feedback && feedback->isCanceled() )
675
        break;
676
    }
677
  }
678
  return results;
679
}
680

    
681
// update the extent, wkb type and srid for this layer, returns false if fails
682
void QgsMssqlProvider::UpdateStatistics( bool estimate ) const
683
{
684
  if ( mGeometryColName.isEmpty() )
685
  {
686
    return;
687
  }
688

    
689
  // get features to calculate the statistics
690
  QString statement;
691

    
692
  QSqlQuery query = createQuery();
693
  query.setForwardOnly( true );
694

    
695

    
696
  if ( mUseGeometryColumnsTableForExtent )
697
  {
698
    if ( !getExtentFromGeometryColumns( mExtent ) )
699
      QgsMessageLog::logMessage( tr( "Invalid extent from geometry_columns table for layer '%1', get extent from the layer." ).arg( mTableName ), tr( "MSSQL" ) );
700
    else
701
      return;
702
  }
703

    
704
  if ( !mIsQuery )
705
  {
706
    // Get the extents from the spatial index table to speed up load times.
707
    // 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.
708
    const QString sql = "SELECT min(bounding_box_xmin), min(bounding_box_ymin), max(bounding_box_xmax), max(bounding_box_ymax)"
709
                        " FROM sys.spatial_index_tessellations WHERE object_id = OBJECT_ID('[%1].[%2]')";
710

    
711
    statement = QString( sql ).arg( mSchemaName, mTableName );
712

    
713
    if ( LoggedExec( query, statement ) )
714
    {
715
      if ( query.next() && ( !QgsVariantUtils::isNull( query.value( 0 ) ) || !QgsVariantUtils::isNull( query.value( 1 ) ) || !QgsVariantUtils::isNull( query.value( 2 ) ) || !QgsVariantUtils::isNull( query.value( 3 ) ) ) )
716
      {
717
        QgsDebugMsgLevel( QStringLiteral( "Found extents in spatial index" ), 2 );
718
        mExtent.setXMinimum( query.value( 0 ).toDouble() );
719
        mExtent.setYMinimum( query.value( 1 ).toDouble() );
720
        mExtent.setXMaximum( query.value( 2 ).toDouble() );
721
        mExtent.setYMaximum( query.value( 3 ).toDouble() );
722
        return;
723
      }
724
    }
725
    else
726
    {
727
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
728
    }
729
  }
730

    
731
  // If we can't find the extents in the spatial index table just do what we normally do.
732
  bool readAllGeography = false;
733
  if ( estimate )
734
  {
735
    if ( mGeometryColType == QLatin1String( "geometry" ) )
736
    {
737
      if ( mDisableInvalidGeometryHandling )
738
        statement = QStringLiteral( "select min(%1.STPointN(1).STX), min(%1.STPointN(1).STY), max(%1.STPointN(1).STX), max(%1.STPointN(1).STY)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
739
      else
740
        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( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
741
    }
742
    else
743
    {
744
      if ( mDisableInvalidGeometryHandling )
745
        statement = QStringLiteral( "select min(%1.STPointN(1).Long), min(%1.STPointN(1).Lat), max(%1.STPointN(1).Long), max(%1.STPointN(1).Lat)" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
746
      else
747
        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( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
748
    }
749

    
750
    // we will first try to sample a small portion of the table/view, so the count of rows involved
751
    // will be useful to evaluate if we have enough data to use the sample
752
    statement += ", count(*)";
753
  }
754
  else
755
  {
756
    if ( mGeometryColType == QLatin1String( "geometry" ) )
757
    {
758
      if ( mDisableInvalidGeometryHandling )
759
        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( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
760
      else
761
        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( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
762
    }
763
    else
764
    {
765
      statement = QStringLiteral( "select %1" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
766
      readAllGeography = true;
767
    }
768
  }
769

    
770
  if ( mIsQuery )
771
  {
772
    statement += QStringLiteral( " FROM (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
773
  }
774
  else
775
  {
776
    statement += QStringLiteral( " FROM %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
777
    if ( !mSqlWhereClause.isEmpty() )
778
    {
779
      statement += " where (" + mSqlWhereClause + ')';
780
    }
781
  }
782

    
783
  if ( estimate )
784
  {
785
    // Try to use just 1% sample of the whole table/view to limit the amount of rows accessed.
786
    // This heuristic may fail (e.g. when the table is small or when primary key values do not
787
    // get sampled enough) so in case we do not have at least 10 features, we fall back to full
788
    // traversal of the table/view
789

    
790
    const int minSampleCount = 10;
791

    
792
    QString cols, delim;
793
    for ( const auto idx : mPrimaryKeyAttrs )
794
    {
795
      const QgsField &fld = mAttributeFields.at( idx );
796
      cols += delim + QgsMssqlUtils::quotedIdentifier( fld.name() );
797
      delim = QStringLiteral( "," );
798
    }
799

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

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

    
805
    if ( LoggedExec( query, statementSample ) && query.next() && !QgsVariantUtils::isNull( query.value( 0 ) ) && query.value( 4 ).toInt() >= minSampleCount )
806
    {
807
      mExtent.setXMinimum( query.value( 0 ).toDouble() );
808
      mExtent.setYMinimum( query.value( 1 ).toDouble() );
809
      mExtent.setXMaximum( query.value( 2 ).toDouble() );
810
      mExtent.setYMaximum( query.value( 3 ).toDouble() );
811
      return;
812
    }
813
  }
814

    
815
  if ( !LoggedExec( query, statement ) )
816
  {
817
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
818
  }
819

    
820
  if ( !query.isActive() )
821
  {
822
    return;
823
  }
824

    
825
  if ( !readAllGeography && query.next() )
826
  {
827
    if ( QgsVariantUtils::isNull( query.value( 0 ) )
828
         || QgsVariantUtils::isNull( query.value( 1 ) )
829
         || QgsVariantUtils::isNull( query.value( 2 ) )
830
         || QgsVariantUtils::isNull( query.value( 3 ) ) )
831
    {
832
      mExtent.setNull();
833
    }
834
    else
835
    {
836
      mExtent.setXMinimum( query.value( 0 ).toDouble() );
837
      mExtent.setYMinimum( query.value( 1 ).toDouble() );
838
      mExtent.setXMaximum( query.value( 2 ).toDouble() );
839
      mExtent.setYMaximum( query.value( 3 ).toDouble() );
840
    }
841
    return;
842
  }
843

    
844
  // We have to read all the geometry if readAllGeography is true.
845
  while ( query.next() )
846
  {
847
    QByteArray ar = query.value( 0 ).toByteArray();
848
    std::unique_ptr<QgsAbstractGeometry> geom = mParser.parseSqlGeometry( reinterpret_cast<unsigned char *>( ar.data() ), ar.size() );
849
    if ( geom )
850
    {
851
      const QgsRectangle rect = geom->boundingBox();
852
      mExtent.combineExtentWith( rect );
853

    
854
      mWkbType = geom->wkbType();
855
      mSRId = mParser.GetSRSId();
856
    }
857
  }
858
}
859

    
860
// Return the extent of the layer
861
QgsRectangle QgsMssqlProvider::extent() const
862
{
863
  if ( mExtent.isNull() )
864
    UpdateStatistics( mUseEstimatedMetadata );
865
  return mExtent;
866
}
867

    
868
/**
869
 * Returns the feature type
870
 */
871
Qgis::WkbType QgsMssqlProvider::wkbType() const
872
{
873
  return mWkbType;
874
}
875

    
876
/**
877
 * Returns the feature type
878
 */
879
long long QgsMssqlProvider::featureCount() const
880
{
881
  // Return the count that we get from the subset.
882
  if ( !mSqlWhereClause.isEmpty() )
883
    return mNumberFeatures;
884

    
885
  // If there is no subset set we can get the count from the system tables.
886
  // Which is faster then doing select count(*)
887
  QSqlQuery query = createQuery();
888
  query.setForwardOnly( true );
889

    
890
  QString statement;
891
  if ( !mIsQuery )
892
  {
893
    statement = QStringLiteral(
894
                  "SELECT rows"
895
                  " FROM sys.tables t"
896
                  " JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1)"
897
                  " WHERE SCHEMA_NAME(t.schema_id) = %1 AND OBJECT_NAME(t.OBJECT_ID) = %2"
898
    )
899
                  .arg( QgsMssqlUtils::quotedValue( mSchemaName ), QgsMssqlUtils::quotedValue( mTableName ) );
900
  }
901
  else
902
  {
903
    statement = { QStringLiteral( R"raw(SELECT COUNT(*) FROM (%1) q)raw" )
904
                    .arg( mQuery ) };
905
  }
906

    
907
  if ( LoggedExec( query, statement ) && query.next() )
908
  {
909
    return query.value( 0 ).toLongLong();
910
  }
911
  else
912
  {
913
    // We couldn't get the rows from the sys tables. Can that ever happen?
914
    // Should just do a select count(*) here.
915
    QgsDebugError( QStringLiteral( "Could not retrieve feature count using %1: %2 " ).arg( statement, query.lastError().text() ) );
916
    return static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
917
  }
918
}
919

    
920
QgsFields QgsMssqlProvider::fields() const
921
{
922
  return mAttributeFields;
923
}
924

    
925
bool QgsMssqlProvider::isValid() const
926
{
927
  return mValid;
928
}
929

    
930
Qgis::ProviderStyleStorageCapabilities QgsMssqlProvider::styleStorageCapabilities() const
931
{
932
  Qgis::ProviderStyleStorageCapabilities storageCapabilities;
933
  if ( isValid() )
934
  {
935
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
936
    storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
937
  }
938
  return storageCapabilities;
939
}
940

    
941
bool QgsMssqlProvider::addFeatures( QgsFeatureList &flist, Flags flags )
942
{
943
  if ( mIsQuery )
944
    return false;
945

    
946
  for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it )
947
  {
948
    if ( it->hasGeometry() && mWkbType == Qgis::WkbType::NoGeometry )
949
    {
950
      it->clearGeometry();
951
    }
952
    else if ( it->hasGeometry() && QgsWkbTypes::geometryType( it->geometry().wkbType() ) != QgsWkbTypes::geometryType( mWkbType ) )
953
    {
954
      pushError( tr( "Could not add feature with geometry type %1 to layer of type %2" ).arg( QgsWkbTypes::displayString( it->geometry().wkbType() ), QgsWkbTypes::displayString( mWkbType ) ) );
955
      if ( !mSkipFailures )
956
        return false;
957

    
958
      continue;
959
    }
960

    
961
    QString statement;
962
    QString values;
963
    if ( !( flags & QgsFeatureSink::FastInsert ) )
964
    {
965
      statement += QLatin1String( "DECLARE @px TABLE (" );
966

    
967
      QString delim;
968
      for ( const auto idx : mPrimaryKeyAttrs )
969
      {
970
        const QgsField &fld = mAttributeFields.at( idx );
971

    
972
        QString type = fld.typeName();
973
        if ( type.endsWith( QLatin1String( " identity" ) ) )
974
          type = type.left( type.length() - 9 );
975
        if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) )
976
        {
977
          if ( fld.length() > 0 )
978
            type = QStringLiteral( "%1(%2)" ).arg( type ).arg( fld.length() );
979
        }
980
        else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
981
        {
982
          if ( fld.length() > 0 && fld.precision() > 0 )
983
            type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( fld.length() ).arg( fld.precision() );
984
        }
985

    
986
        statement += delim + QStringLiteral( "[%1] %2" ).arg( fld.name(), type );
987
        delim = ",";
988
      }
989

    
990
      statement += "); ";
991
    }
992

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

    
995
    bool first = true;
996
    QSqlQuery query = createQuery();
997
    query.setForwardOnly( true );
998

    
999
    const QgsAttributes attrs = it->attributes();
1000

    
1001
    for ( int i = 0; i < attrs.count(); ++i )
1002
    {
1003
      if ( i >= mAttributeFields.count() )
1004
        break;
1005

    
1006
      const QgsField &fld = mAttributeFields.at( i );
1007

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

    
1011
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1012
        continue; // skip identity field
1013

    
1014
      if ( fld.name().isEmpty() )
1015
        continue; // invalid
1016

    
1017
      if ( QgsVariantUtils::isUnsetAttributeValue( attrs.at( i ) ) )
1018
        continue;
1019

    
1020
      if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
1021
        continue; // skip fields having default values
1022

    
1023
      if ( mComputedColumns.contains( fld.name() ) )
1024
        continue; // skip computed columns because they are done server side.
1025

    
1026
      if ( !first )
1027
      {
1028
        statement += ',';
1029
        values += ',';
1030
      }
1031
      else
1032
        first = false;
1033

    
1034
      statement += QStringLiteral( "[%1]" ).arg( fld.name() );
1035
      values += QLatin1Char( '?' );
1036
    }
1037

    
1038
    // append geometry column name
1039
    if ( !mGeometryColName.isEmpty() )
1040
    {
1041
      if ( !first )
1042
      {
1043
        statement += ',';
1044
        values += ',';
1045
      }
1046

    
1047
      statement += QStringLiteral( "[%1]" ).arg( mGeometryColName );
1048
      if ( mGeometryColType == QLatin1String( "geometry" ) )
1049
      {
1050
        if ( mUseWkb )
1051
          values += QStringLiteral( "geometry::STGeomFromWKB(?,%1).MakeValid()" ).arg( mSRId );
1052
        else
1053
          values += QStringLiteral( "geometry::STGeomFromText(?,%1).MakeValid()" ).arg( mSRId );
1054
      }
1055
      else
1056
      {
1057
        if ( mUseWkb )
1058
          values += QStringLiteral( "geography::STGeomFromWKB(?,%1)" ).arg( mSRId );
1059
        else
1060
          values += QStringLiteral( "geography::STGeomFromText(?,%1)" ).arg( mSRId );
1061
      }
1062
    }
1063

    
1064
    statement += QLatin1String( ") " );
1065
    if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
1066
    {
1067
      statement += QLatin1String( " OUTPUT " );
1068

    
1069
      QString delim;
1070

    
1071
      for ( const auto idx : std::as_const( mPrimaryKeyAttrs ) )
1072
      {
1073
        const QgsField &fld = mAttributeFields.at( idx );
1074
        statement += delim + QStringLiteral( "inserted.[%1]" ).arg( fld.name() );
1075
        delim = QStringLiteral( "," );
1076
      }
1077

    
1078
      statement += QLatin1String( " INTO @px " );
1079
    }
1080

    
1081
    statement += QStringLiteral( " VALUES (" ) + values + ')';
1082

    
1083
    if ( !( flags & QgsFeatureSink::FastInsert && !mPrimaryKeyAttrs.isEmpty() ) )
1084
    {
1085
      statement += QLatin1String( "; SELECT * FROM @px;" );
1086
    }
1087

    
1088
    // use prepared statement to prevent from sql injection
1089
    if ( !query.prepare( statement ) )
1090
    {
1091
      const QString msg = query.lastError().text();
1092
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1093
      if ( !mSkipFailures )
1094
      {
1095
        pushError( msg );
1096
        return false;
1097
      }
1098
      else
1099
        continue;
1100
    }
1101

    
1102
    for ( int i = 0; i < attrs.count(); ++i )
1103
    {
1104
      if ( i >= mAttributeFields.count() )
1105
        break;
1106

    
1107
      const QgsField &fld = mAttributeFields.at( i );
1108

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

    
1112
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1113
        continue; // skip identity field
1114

    
1115
      if ( fld.name().isEmpty() )
1116
        continue; // invalid
1117

    
1118
      if ( QgsVariantUtils::isUnsetAttributeValue( attrs.at( i ) ) )
1119
        continue;
1120

    
1121
      if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
1122
        continue; // skip fields having default values
1123

    
1124
      if ( mComputedColumns.contains( fld.name() ) )
1125
        continue; // skip computed columns because they are done server side.
1126

    
1127
      const QMetaType::Type type = fld.type();
1128
      if ( QgsVariantUtils::isNull( attrs.at( i ) ) )
1129
      {
1130
        // binding null values
1131
        if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
1132
          query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
1133
        else
1134
          query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
1135
      }
1136
      else if ( type == QMetaType::Type::Int )
1137
      {
1138
        // binding an INTEGER value
1139
        query.addBindValue( attrs.at( i ).toInt() );
1140
      }
1141
      else if ( type == QMetaType::Type::Double )
1142
      {
1143
        // binding a DOUBLE value
1144
        query.addBindValue( attrs.at( i ).toDouble() );
1145
      }
1146
      else if ( type == QMetaType::Type::QString )
1147
      {
1148
        // binding a TEXT value
1149
        query.addBindValue( attrs.at( i ).toString() );
1150
      }
1151
      else if ( type == QMetaType::Type::QTime )
1152
      {
1153
        // binding a TIME value
1154
        query.addBindValue( attrs.at( i ).toTime().toString( Qt::ISODate ) );
1155
      }
1156
      else if ( type == QMetaType::Type::QDate )
1157
      {
1158
        // binding a DATE value
1159
        query.addBindValue( attrs.at( i ).toDate().toString( Qt::ISODate ) );
1160
      }
1161
      else if ( type == QMetaType::Type::QDateTime )
1162
      {
1163
        // binding a DATETIME value
1164
        query.addBindValue( attrs.at( i ).toDateTime().toString( Qt::ISODate ) );
1165
      }
1166
      else
1167
      {
1168
        query.addBindValue( attrs.at( i ) );
1169
      }
1170
    }
1171

    
1172
    if ( !mGeometryColName.isEmpty() )
1173
    {
1174
      QgsGeometry geom = it->geometry();
1175
      if ( QgsWkbTypes::isMultiType( mWkbType ) && !geom.isMultipart() )
1176
      {
1177
        geom.convertToMultiType();
1178
      }
1179
      if ( mUseWkb )
1180
      {
1181
        const QByteArray bytea = geom.asWkb();
1182
        query.addBindValue( bytea, QSql::In | QSql::Binary );
1183
      }
1184
      else
1185
      {
1186
        QString wkt;
1187
        if ( !geom.isNull() )
1188
        {
1189
          // Z and M on the end of a WKT string isn't valid for
1190
          // SQL Server so we have to remove it first.
1191
          wkt = geom.asWkt();
1192
          const thread_local QRegularExpression wktRx( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
1193
          wkt.replace( wktRx, QStringLiteral( "(" ) );
1194
          // if we have M value only, we need to insert null-s for the Z value
1195
          if ( QgsWkbTypes::hasM( geom.wkbType() ) && !QgsWkbTypes::hasZ( geom.wkbType() ) )
1196
          {
1197
            const thread_local QRegularExpression nullRx( QStringLiteral( "(?=\\s[0-9+-.]+[,)])" ) );
1198
            wkt.replace( QRegularExpression( nullRx ), QStringLiteral( " NULL" ) );
1199
          }
1200
        }
1201
        query.addBindValue( wkt );
1202
      }
1203
    }
1204

    
1205
    if ( !LoggedExecPrepared( query ) )
1206
    {
1207
      const QString msg = query.lastError().text();
1208
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1209
      if ( !mSkipFailures )
1210
      {
1211
        pushError( msg );
1212
        return false;
1213
      }
1214
    }
1215

    
1216
    if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
1217
    {
1218
      if ( !query.next() )
1219
      {
1220
        const QString msg = query.lastError().text();
1221
        QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1222
        if ( !mSkipFailures )
1223
        {
1224
          pushError( msg );
1225
          return false;
1226
        }
1227
      }
1228

    
1229
      if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::Int )
1230
      {
1231
        it->setId( query.value( 0 ).toLongLong() );
1232
      }
1233
      else
1234
      {
1235
        QVariantList keyvals;
1236
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
1237
        {
1238
          keyvals << query.value( i );
1239
        }
1240

    
1241
        it->setId( mShared->lookupFid( keyvals ) );
1242
      }
1243
    }
1244
  }
1245

    
1246
  if ( mTransaction )
1247
    mTransaction->dirtyLastSavePoint();
1248

    
1249
  return true;
1250
}
1251

    
1252
bool QgsMssqlProvider::addAttributes( const QList<QgsField> &attributes )
1253
{
1254
  if ( attributes.isEmpty() )
1255
    return true;
1256

    
1257
  if ( mIsQuery )
1258
    return false;
1259

    
1260
  QString statement = QStringLiteral( "ALTER TABLE %1.%2 ADD " ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
1261

    
1262
  QStringList attributeClauses;
1263
  attributeClauses.reserve( attributes.size() );
1264
  for ( QList<QgsField>::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
1265
  {
1266
    QString type = it->typeName();
1267
    if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) || type == QLatin1String( "nvarchar" ) )
1268
    {
1269
      if ( it->length() > 0 )
1270
        type = QStringLiteral( "%1(%2)" ).arg( type ).arg( it->length() );
1271
    }
1272
    else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
1273
    {
1274
      if ( it->length() > 0 && it->precision() > 0 )
1275
        type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( it->length() ).arg( it->precision() );
1276
    }
1277

    
1278
    attributeClauses.append( QStringLiteral( "[%1] %2" ).arg( it->name(), type ) );
1279
  }
1280
  statement += attributeClauses.join( QStringLiteral( ", " ) );
1281

    
1282
  QSqlQuery query = createQuery();
1283
  query.setForwardOnly( true );
1284
  if ( !LoggedExec( query, statement ) )
1285
  {
1286
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1287
    return false;
1288
  }
1289

    
1290
  loadFields();
1291

    
1292
  if ( mTransaction )
1293
    mTransaction->dirtyLastSavePoint();
1294

    
1295
  return true;
1296
}
1297

    
1298
bool QgsMssqlProvider::deleteAttributes( const QgsAttributeIds &attributes )
1299
{
1300
  if ( mIsQuery )
1301
    return false;
1302

    
1303
  QString statement;
1304

    
1305
  for ( QgsAttributeIds::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
1306
  {
1307
    if ( statement.isEmpty() )
1308
    {
1309
      statement = QStringLiteral( "ALTER TABLE [%1].[%2] DROP COLUMN " ).arg( mSchemaName, mTableName );
1310
    }
1311
    else
1312
      statement += ',';
1313

    
1314
    statement += QStringLiteral( "[%1]" ).arg( mAttributeFields.at( *it ).name() );
1315
  }
1316

    
1317
  QSqlQuery query = createQuery();
1318
  query.setForwardOnly( true );
1319

    
1320
  if ( !LoggedExec( query, statement ) )
1321
  {
1322
    QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1323
    return false;
1324
  }
1325

    
1326
  query.finish();
1327

    
1328
  loadFields();
1329

    
1330
  if ( mTransaction )
1331
    mTransaction->dirtyLastSavePoint();
1332

    
1333
  return true;
1334
}
1335

    
1336

    
1337
bool QgsMssqlProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
1338
{
1339
  if ( mIsQuery )
1340
    return false;
1341

    
1342
  if ( attr_map.isEmpty() )
1343
    return true;
1344

    
1345
  if ( mPrimaryKeyAttrs.isEmpty() )
1346
    return false;
1347

    
1348
  for ( QgsChangedAttributesMap::const_iterator it = attr_map.begin(); it != attr_map.end(); ++it )
1349
  {
1350
    const QgsFeatureId fid = it.key();
1351

    
1352
    // skip added features
1353
    if ( FID_IS_NEW( fid ) )
1354
      continue;
1355

    
1356
    const QgsAttributeMap &attrs = it.value();
1357
    if ( attrs.isEmpty() )
1358
      continue;
1359

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

    
1362
    bool first = true;
1363
    bool pkChanged = false;
1364
    QSqlQuery query = createQuery();
1365
    query.setForwardOnly( true );
1366

    
1367
    for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
1368
    {
1369
      const QgsField fld = mAttributeFields.at( it2.key() );
1370

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

    
1374
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1375
        continue; // skip identity field
1376

    
1377
      if ( fld.name().isEmpty() )
1378
        continue; // invalid
1379

    
1380
      if ( QgsVariantUtils::isUnsetAttributeValue( it2.value() ) )
1381
        continue;
1382

    
1383
      if ( mComputedColumns.contains( fld.name() ) )
1384
        continue; // skip computed columns because they are done server side.
1385

    
1386
      if ( !first )
1387
        statement += ',';
1388
      else
1389
        first = false;
1390

    
1391
      pkChanged = pkChanged || mPrimaryKeyAttrs.contains( it2.key() );
1392

    
1393
      statement += QStringLiteral( "[%1]=?" ).arg( fld.name() );
1394
    }
1395

    
1396
    if ( first )
1397
      return true; // no fields have been changed
1398

    
1399
    // set attribute filter
1400
    statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
1401

    
1402
    // use prepared statement to prevent from sql injection
1403
    if ( !query.prepare( statement ) )
1404
    {
1405
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1406
      return false;
1407
    }
1408

    
1409
    for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
1410
    {
1411
      const QgsField fld = mAttributeFields.at( it2.key() );
1412

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

    
1416
      if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
1417
        continue; // skip identity field
1418

    
1419
      if ( fld.name().isEmpty() )
1420
        continue; // invalid
1421

    
1422
      if ( it2.value().userType() == qMetaTypeId< QgsUnsetAttributeValue >() )
1423
        continue;
1424

    
1425
      if ( mComputedColumns.contains( fld.name() ) )
1426
        continue; // skip computed columns because they are done server side.
1427

    
1428
      const QMetaType::Type type = fld.type();
1429
      if ( QgsVariantUtils::isNull( *it2 ) )
1430
      {
1431
        // binding null values
1432
        if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
1433
          query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
1434
        else
1435
          query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
1436
      }
1437
      else if ( type == QMetaType::Type::Int )
1438
      {
1439
        // binding an INTEGER value
1440
        query.addBindValue( it2->toInt() );
1441
      }
1442
      else if ( type == QMetaType::Type::Double )
1443
      {
1444
        // binding a DOUBLE value
1445
        query.addBindValue( it2->toDouble() );
1446
      }
1447
      else if ( type == QMetaType::Type::QString )
1448
      {
1449
        // binding a TEXT value
1450
        query.addBindValue( it2->toString() );
1451
      }
1452
      else if ( type == QMetaType::Type::QDateTime )
1453
      {
1454
        // binding a DATETIME value
1455
        query.addBindValue( it2->toDateTime().toString( Qt::ISODate ) );
1456
      }
1457
      else if ( type == QMetaType::Type::QDate )
1458
      {
1459
        // binding a DATE value
1460
        query.addBindValue( it2->toDate().toString( Qt::ISODate ) );
1461
      }
1462
      else if ( type == QMetaType::Type::QTime )
1463
      {
1464
        // binding a TIME value
1465
        query.addBindValue( it2->toTime().toString( Qt::ISODate ) );
1466
      }
1467
      else
1468
      {
1469
        query.addBindValue( *it2 );
1470
      }
1471
    }
1472

    
1473
    if ( !LoggedExecPrepared( query ) )
1474
    {
1475
      QgsDebugError( QStringLiteral( "SQL:%1\n  Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
1476
      return false;
1477
    }
1478

    
1479
    if ( pkChanged && mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::FidMap )
1480
    {
1481
      const QVariant v = mShared->removeFid( fid );
1482
      QVariantList k = v.toList();
1483

    
1484
      for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
1485
      {
1486
        const int idx = mPrimaryKeyAttrs[i];
1487
        if ( !attrs.contains( idx ) )
1488
          continue;
1489

    
1490
        k[i] = attrs[idx];
1491
      }
1492

    
1493
      mShared->insertFid( fid, k );
1494
    }
1495
  }
1496

    
1497
  if ( mTransaction )
1498
    mTransaction->dirtyLastSavePoint();
1499

    
1500
  return true;
1501
}
1502

    
1503
bool QgsMssqlProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
1504
{
1505
  if ( mIsQuery )
1506
    return false;
1507

    
1508
  if ( geometry_map.isEmpty() )
1509
    return true;
1510

    
1511
  if ( mPrimaryKeyAttrs.isEmpty() )
1512
    return false;
1513

    
1514
  for ( QgsGeometryMap::const_iterator it = geometry_map.constBegin(); it != geometry_map.constEnd(); ++it )
1515
  {
1516
    const QgsFeatureId fid = it.key();
1517
    // skip added features
1518
    if ( FID_IS_NEW( fid ) )
1519
      continue;
1520

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

    
1524
    QSqlQuery query = createQuery();
1525
    query.setForwardOnly( true );
1526

    
1527
    if ( mGeometryColType == QLatin1String( "geometry" ) )
1528
    {
1529
      if ( mUseWkb )
1530
        statement += QStringLiteral( "[%1]=geometry::STGeomFromWKB(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
1531
      else
1532
        statement += QStringLiteral( "[%1]=geometry::STGeomFromText(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
1533
    }
1534
    else
1535
    {
1536
      if ( mUseWkb )
1537
        statement += QStringLiteral( "[%1]=geography::STGeomFromWKB(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
1538
      else
1539
        statement += QStringLiteral( "[%1]=geography::STGeomFromText(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
1540
    }
1541

    
1542
    // set attribute filter
1543
    statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
1544

    
1545
    if ( !query.prepare( statement ) )
1546
    {
1547
      pushError( query.lastError().text() );
1548
      return false;
1549
    }
1550

    
1551
    // add geometry param
1552
    if ( mUseWkb )
1553
    {
1554
      const QByteArray bytea = it->asWkb();
1555
      query.addBindValue( bytea, QSql::In | QSql::Binary );
1556
    }
1557
    else
1558
    {
1559
      QString wkt = it->asWkt();
1560
      // Z and M on the end of a WKT string isn't valid for
1561
      // SQL Server so we have to remove it first.
1562
      const thread_local QRegularExpression zmRegExp( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
1563
      wkt.replace( zmRegExp, QStringLiteral( "(" ) );
1564
      query.addBindValue( wkt );
1565
    }
1566

    
1567
    if ( !LoggedExecPrepared( query ) )
1568
    {
1569
      pushError( query.lastError().text() );
1570
      return false;
1571
    }
1572
  }
1573

    
1574
  if ( mTransaction )
1575
    mTransaction->dirtyLastSavePoint();
1576

    
1577
  return true;
1578
}
1579

    
1580
bool QgsMssqlProvider::deleteFeatures( const QgsFeatureIds &ids )
1581
{
1582
  if ( mIsQuery )
1583
    return false;
1584

    
1585
  if ( mPrimaryKeyAttrs.isEmpty() )
1586
    return false;
1587

    
1588
  if ( ids.empty() )
1589
    return true; // for consistency providers return true to an empty list
1590

    
1591
  if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::Int )
1592
  {
1593
    QString featureIds, delim;
1594
    for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
1595
    {
1596
      featureIds += delim + FID_TO_STRING( *it );
1597
      delim = QStringLiteral( "," );
1598
    }
1599

    
1600
    QSqlQuery query = createQuery();
1601
    query.setForwardOnly( true );
1602

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

    
1605
    if ( LoggedExec( query, statement ) )
1606
    {
1607
      if ( query.numRowsAffected() == ids.size() )
1608
      {
1609
        if ( mTransaction )
1610
          mTransaction->dirtyLastSavePoint();
1611
        return true;
1612
      }
1613

    
1614
      pushError( tr( "Only %1 of %2 features deleted" ).arg( query.numRowsAffected() ).arg( ids.size() ) );
1615
    }
1616
    else
1617
    {
1618
      pushError( query.lastError().text() );
1619
    }
1620
  }
1621
  else if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::FidMap )
1622
  {
1623
    int i = 0;
1624

    
1625
    QSqlQuery query = createQuery();
1626
    for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
1627
    {
1628
      const QString statement = QStringLiteral( "DELETE FROM [%1].[%2] WHERE %3" ).arg( mSchemaName, mTableName, whereClauseFid( *it ) );
1629

    
1630
      if ( LoggedExec( query, statement ) )
1631
      {
1632
        if ( query.numRowsAffected() == 1 )
1633
        {
1634
          mShared->removeFid( *it );
1635
          i++;
1636
        }
1637
      }
1638
      else
1639
      {
1640
        pushError( query.lastError().text() );
1641
        break;
1642
      }
1643
    }
1644

    
1645
    if ( i == ids.size() )
1646
    {
1647
      if ( mTransaction )
1648
        mTransaction->dirtyLastSavePoint();
1649
      return true;
1650
    }
1651

    
1652
    if ( i > 0 )
1653
      pushError( tr( "Only %1 of %2 features deleted" ).arg( i ).arg( ids.size() ) );
1654
  }
1655

    
1656
  return false;
1657
}
1658

    
1659
void QgsMssqlProvider::updateExtents()
1660
{
1661
  mExtent.setNull();
1662
}
1663

    
1664
Qgis::VectorProviderCapabilities QgsMssqlProvider::capabilities() const
1665
{
1666
  Qgis::VectorProviderCapabilities cap;
1667
  const bool hasGeom = !mGeometryColName.isEmpty();
1668
  if ( !mIsQuery )
1669
  {
1670
    cap |= Qgis::VectorProviderCapability::CreateAttributeIndex | Qgis::VectorProviderCapability::AddFeatures | Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::TransactionSupport;
1671
    if ( hasGeom )
1672
    {
1673
      cap |= Qgis::VectorProviderCapability::CreateSpatialIndex;
1674
    }
1675
  }
1676

    
1677
  if ( mPrimaryKeyAttrs.isEmpty() )
1678
    return cap;
1679

    
1680
  cap |= Qgis::VectorProviderCapability::SelectAtId;
1681

    
1682
  if ( hasGeom && !mIsQuery )
1683
    cap |= Qgis::VectorProviderCapability::ChangeGeometries;
1684
  if ( !mIsQuery )
1685
    cap |= Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::DeleteAttributes;
1686

    
1687
  return cap;
1688
}
1689

    
1690
bool QgsMssqlProvider::createSpatialIndex()
1691
{
1692
  if ( mUseEstimatedMetadata )
1693
    UpdateStatistics( false );
1694

    
1695
  QSqlQuery query = createQuery();
1696
  query.setForwardOnly( true );
1697
  QString statement;
1698
  statement = QStringLiteral( "CREATE SPATIAL INDEX [qgs_%1_sidx] ON [%2].[%3] ( [%4] )" ).arg( mGeometryColName, mSchemaName, mTableName, mGeometryColName );
1699

    
1700
  if ( mGeometryColType == QLatin1String( "geometry" ) )
1701
  {
1702
    if ( mExtent.isNull() )
1703
      return false;
1704

    
1705
    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() ) );
1706
  }
1707
  else
1708
  {
1709
    statement += QLatin1String( " USING GEOGRAPHY_GRID" );
1710
  }
1711

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

    
1718
  return true;
1719
}
1720

    
1721
bool QgsMssqlProvider::createAttributeIndex( int field )
1722
{
1723
  QSqlQuery query = createQuery();
1724
  query.setForwardOnly( true );
1725
  QString statement;
1726

    
1727
  if ( field < 0 || field >= mAttributeFields.size() )
1728
  {
1729
    pushError( QStringLiteral( "createAttributeIndex invalid index" ) );
1730
    return false;
1731
  }
1732

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

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

    
1741
  return true;
1742
}
1743

    
1744
QgsCoordinateReferenceSystem QgsMssqlProvider::crs() const
1745
{
1746
  if ( !mCrs.isValid() && mSRId > 0 )
1747
  {
1748
    // try to load crs from the database tables as a fallback
1749
    QSqlQuery query = createQuery();
1750
    query.setForwardOnly( true );
1751
    const QString statement { QStringLiteral( "SELECT srtext FROM spatial_ref_sys WHERE srid=%1" ).arg( mSRId ) };
1752

    
1753
    bool execOk = LoggedExec( query, statement );
1754
    if ( execOk && query.isActive() )
1755
    {
1756
      if ( query.next() )
1757
      {
1758
        mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
1759
        if ( mCrs.isValid() )
1760
          return mCrs;
1761
      }
1762

    
1763
      query.finish();
1764
    }
1765
    query.clear();
1766
    query.setForwardOnly( true );
1767

    
1768
    // Look in the system reference table for the data if we can't find it yet
1769
    execOk = LoggedExec( query, QStringLiteral( "SELECT well_known_text FROM sys.spatial_reference_systems WHERE spatial_reference_id=%1" ).arg( mSRId ) );
1770
    if ( execOk && query.isActive() && query.next() )
1771
    {
1772
      mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
1773
      if ( mCrs.isValid() )
1774
        return mCrs;
1775
    }
1776
    else // try to load as EPSG
1777
    {
1778
      mCrs = QgsCoordinateReferenceSystem::fromEpsgId( mSRId );
1779
    }
1780
  }
1781
  return mCrs;
1782
}
1783

    
1784

    
1785
void QgsMssqlProvider::setTransaction( QgsTransaction *transaction )
1786
{
1787
  // static_cast since layers cannot be added to a transaction of a non-matching provider
1788
  mTransaction = static_cast<QgsMssqlTransaction *>( transaction );
1789
}
1790

    
1791
QgsTransaction *QgsMssqlProvider::transaction() const
1792
{
1793
  return mTransaction;
1794
}
1795

    
1796
std::shared_ptr<QgsMssqlDatabase> QgsMssqlProvider::connection() const
1797
{
1798
  return mTransaction ? mTransaction->conn() : QgsMssqlDatabase::connectDb( uri().connectionInfo(), false );
1799
}
1800

    
1801
void QgsMssqlProvider::handlePostCloneOperations( QgsVectorDataProvider *source )
1802
{
1803
  mShared = qobject_cast<QgsMssqlProvider *>( source )->mShared;
1804
}
1805

    
1806
QString QgsMssqlProvider::subsetString() const
1807
{
1808
  return mSqlWhereClause;
1809
}
1810

    
1811
QString QgsMssqlProvider::name() const
1812
{
1813
  return MSSQL_PROVIDER_KEY;
1814
}
1815

    
1816
bool QgsMssqlProvider::setSubsetString( const QString &theSQL, bool )
1817
{
1818
  if ( theSQL.trimmed() == mSqlWhereClause )
1819
    return true;
1820

    
1821
  const QString prevWhere = mSqlWhereClause;
1822

    
1823
  mSqlWhereClause = theSQL.trimmed();
1824

    
1825
  QString sql;
1826
  if ( mIsQuery )
1827
  {
1828
    sql = QStringLiteral( "SELECT count(*) FROM %1 q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
1829
  }
1830
  else
1831
  {
1832
    sql = QStringLiteral( "SELECT count(*) FROM %1.%2 %3" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ), !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
1833
  }
1834

    
1835
  QSqlQuery query = createQuery();
1836
  query.setForwardOnly( true );
1837
  if ( !LoggedExec( query, sql ) )
1838
  {
1839
    pushError( query.lastError().text() );
1840
    mSqlWhereClause = prevWhere;
1841
    return false;
1842
  }
1843

    
1844
  if ( query.isActive() && query.next() )
1845
    mNumberFeatures = query.value( 0 ).toLongLong();
1846

    
1847
  QgsDataSourceUri anUri = QgsDataSourceUri( dataSourceUri() );
1848
  anUri.setSql( mSqlWhereClause );
1849

    
1850
  setDataSourceUri( anUri.uri() );
1851

    
1852
  mExtent.setNull();
1853

    
1854
  emit dataChanged();
1855

    
1856
  return true;
1857
}
1858

    
1859
bool QgsMssqlProvider::supportsSubsetString() const
1860
{
1861
  return true;
1862
}
1863

    
1864
QString QgsMssqlProvider::subsetStringDialect() const
1865
{
1866
  return tr( "SQL Server WHERE clause" );
1867
}
1868

    
1869
QString QgsMssqlProvider::subsetStringHelpUrl() const
1870
{
1871
  return QStringLiteral( "https://learn.microsoft.com/en-us/sql/t-sql/queries/where-transact-sql?view=sql-server-ver16" );
1872
}
1873

    
1874
QString QgsMssqlProvider::description() const
1875
{
1876
  return MSSQL_PROVIDER_DESCRIPTION;
1877
}
1878

    
1879
QgsAttributeList QgsMssqlProvider::pkAttributeIndexes() const
1880
{
1881
  return mPrimaryKeyAttrs;
1882
}
1883

    
1884
QString QgsMssqlProvider::geometryColumnName() const
1885
{
1886
  QGIS_PROTECT_QOBJECT_THREAD_ACCESS
1887

    
1888
  return mGeometryColName;
1889
}
1890

    
1891
QStringList QgsMssqlProvider::subLayers() const
1892
{
1893
  return mTables;
1894
}
1895

    
1896
Qgis::VectorLayerTypeFlags QgsMssqlProvider::vectorLayerTypeFlags() const
1897
{
1898
  Qgis::VectorLayerTypeFlags flags;
1899
  if ( mValid && mIsQuery )
1900
  {
1901
    flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery );
1902
  }
1903
  return flags;
1904
}
1905

    
1906
bool QgsMssqlProvider::convertField( QgsField &field )
1907
{
1908
  QString fieldType = QStringLiteral( "nvarchar(max)" ); //default to string
1909
  int fieldSize = field.length();
1910
  int fieldPrec = field.precision();
1911
  switch ( field.type() )
1912
  {
1913
    case QMetaType::Type::LongLong:
1914
      fieldType = QStringLiteral( "bigint" );
1915
      fieldSize = -1;
1916
      fieldPrec = 0;
1917
      break;
1918

    
1919
    case QMetaType::Type::QDateTime:
1920
      fieldType = QStringLiteral( "datetime" );
1921
      fieldPrec = 0;
1922
      break;
1923

    
1924
    case QMetaType::Type::QDate:
1925
      fieldType = QStringLiteral( "date" );
1926
      fieldPrec = 0;
1927
      break;
1928

    
1929
    case QMetaType::Type::QTime:
1930
      fieldType = QStringLiteral( "time" );
1931
      fieldPrec = 0;
1932
      break;
1933

    
1934
    case QMetaType::Type::QString:
1935
      fieldType = QStringLiteral( "nvarchar(max)" );
1936
      fieldPrec = 0;
1937
      break;
1938

    
1939
    case QMetaType::Type::Int:
1940
      fieldType = QStringLiteral( "int" );
1941
      fieldSize = -1;
1942
      fieldPrec = 0;
1943
      break;
1944

    
1945
    case QMetaType::Type::Double:
1946
      if ( fieldSize <= 0 || fieldPrec <= 0 )
1947
      {
1948
        fieldType = QStringLiteral( "float" );
1949
        fieldSize = -1;
1950
        fieldPrec = 0;
1951
      }
1952
      else
1953
      {
1954
        fieldType = QStringLiteral( "decimal" );
1955
      }
1956
      break;
1957

    
1958
    default:
1959
      return false;
1960
  }
1961

    
1962
  field.setTypeName( fieldType );
1963
  field.setLength( fieldSize );
1964
  field.setPrecision( fieldPrec );
1965
  return true;
1966
}
1967

    
1968
void QgsMssqlProvider::mssqlWkbTypeAndDimension( Qgis::WkbType wkbType, QString &geometryType, int &dim )
1969
{
1970
  const Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
1971

    
1972
  if ( flatType == Qgis::WkbType::Point )
1973
    geometryType = QStringLiteral( "POINT" );
1974
  else if ( flatType == Qgis::WkbType::LineString )
1975
    geometryType = QStringLiteral( "LINESTRING" );
1976
  else if ( flatType == Qgis::WkbType::Polygon )
1977
    geometryType = QStringLiteral( "POLYGON" );
1978
  else if ( flatType == Qgis::WkbType::MultiPoint )
1979
    geometryType = QStringLiteral( "MULTIPOINT" );
1980
  else if ( flatType == Qgis::WkbType::MultiLineString )
1981
    geometryType = QStringLiteral( "MULTILINESTRING" );
1982
  else if ( flatType == Qgis::WkbType::MultiPolygon )
1983
    geometryType = QStringLiteral( "MULTIPOLYGON" );
1984
  else if ( flatType == Qgis::WkbType::GeometryCollection )
1985
    geometryType = QStringLiteral( "GEOMETRYCOLLECTION" );
1986
  else if ( flatType == Qgis::WkbType::CircularString )
1987
    geometryType = QStringLiteral( "CIRCULARSTRING" );
1988
  else if ( flatType == Qgis::WkbType::CompoundCurve )
1989
    geometryType = QStringLiteral( "COMPOUNDCURVE" );
1990
  else if ( flatType == Qgis::WkbType::CurvePolygon )
1991
    geometryType = QStringLiteral( "CURVEPOLYGON" );
1992
  else if ( flatType == Qgis::WkbType::Unknown )
1993
    geometryType = QStringLiteral( "GEOMETRY" );
1994
  else
1995
  {
1996
    dim = 0;
1997
    return;
1998
  }
1999

    
2000
  if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
2001
  {
2002
    dim = 4;
2003
  }
2004
  else if ( QgsWkbTypes::hasZ( wkbType ) )
2005
  {
2006
    dim = 3;
2007
  }
2008
  else if ( QgsWkbTypes::hasM( wkbType ) )
2009
  {
2010
    geometryType += QLatin1Char( 'M' );
2011
    dim = 3;
2012
  }
2013
  else if ( wkbType >= Qgis::WkbType::Point25D && wkbType <= Qgis::WkbType::MultiPolygon25D )
2014
  {
2015
    dim = 3;
2016
  }
2017
}
2018

    
2019
Qgis::WkbType QgsMssqlProvider::getWkbType( const QString &geometryType )
2020
{
2021
  return QgsWkbTypes::parseType( geometryType );
2022
}
2023

    
2024

    
2025
Qgis::VectorExportResult QgsMssqlProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap<int, int> *oldToNewAttrIdxMap, QString &createdLayerUri, QString *errorMessage, const QMap<QString, QVariant> *options )
2026
{
2027
  // populate members from the uri structure
2028
  QgsDataSourceUri dsUri( uri );
2029

    
2030
  // connect to database
2031
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
2032

    
2033
  if ( !db->isValid() )
2034
  {
2035
    if ( errorMessage )
2036
      *errorMessage = db->errorText();
2037
    return Qgis::VectorExportResult::ErrorConnectionFailed;
2038
  }
2039

    
2040
  createdLayerUri = uri;
2041

    
2042
  const QString dbName = dsUri.database();
2043

    
2044
  QString schemaName = dsUri.schema();
2045
  const QString tableName = dsUri.table();
2046

    
2047
  QString geometryColumn = dsUri.geometryColumn();
2048

    
2049
  QString primaryKey = dsUri.keyColumn();
2050

    
2051
  if ( schemaName.isEmpty() )
2052
    schemaName = QStringLiteral( "dbo" );
2053

    
2054
  if ( wkbType != Qgis::WkbType::NoGeometry && geometryColumn.isEmpty() )
2055
    geometryColumn = QStringLiteral( "geom" );
2056

    
2057
  // if no pk name was passed, define the new pk field name
2058
  if ( primaryKey.isEmpty() )
2059
  {
2060
    int index = 0;
2061
    const QString pk = primaryKey = QStringLiteral( "qgs_fid" );
2062
    for ( int i = 0, n = fields.size(); i < n; ++i )
2063
    {
2064
      if ( fields.at( i ).name() == primaryKey )
2065
      {
2066
        // it already exists, try again with a new name
2067
        primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ );
2068
        i = 0;
2069
      }
2070
    }
2071
  }
2072

    
2073
  QString sql;
2074
  QSqlQuery q = QSqlQuery( db->db() );
2075
  q.setForwardOnly( true );
2076

    
2077
  // initialize metadata tables (same as OGR SQL)
2078
  sql = QString( "IF OBJECT_ID(N'[geometry_columns]', N'U') IS NULL "
2079
                 "CREATE TABLE geometry_columns (f_table_catalog varchar(128) not null, "
2080
                 "f_table_schema varchar(128) not null, f_table_name varchar(256) not null, "
2081
                 "f_geometry_column varchar(256) not null, coord_dimension integer not null, "
2082
                 "srid integer not null, geometry_type varchar(30) not null, "
2083
                 "CONSTRAINT geometry_columns_pk PRIMARY KEY (f_table_catalog, "
2084
                 "f_table_schema, f_table_name, f_geometry_column));\n"
2085
                 "IF OBJECT_ID(N'[spatial_ref_sys]', N'U') IS NULL "
2086
                 "CREATE TABLE spatial_ref_sys (srid integer not null "
2087
                 "PRIMARY KEY, auth_name varchar(256), auth_srid integer, srtext varchar(2048), proj4text varchar(2048))" );
2088

    
2089
  auto logWrapper = std::make_unique<QgsDatabaseQueryLogWrapper>( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN );
2090

    
2091
  if ( !q.exec( sql ) )
2092
  {
2093
    logWrapper->setError( q.lastError().text() );
2094
    if ( errorMessage )
2095
      *errorMessage = q.lastError().text();
2096
    return Qgis::VectorExportResult::ErrorCreatingLayer;
2097
  }
2098

    
2099
  // set up spatial reference id
2100
  long srid = 0;
2101
  if ( srs.isValid() )
2102
  {
2103
    srid = srs.postgisSrid();
2104
    QString auth_srid = QStringLiteral( "null" );
2105
    QString auth_name = QStringLiteral( "null" );
2106
    QStringList sl = srs.authid().split( ':' );
2107
    if ( sl.length() == 2 )
2108
    {
2109
      auth_name = sl[0];
2110
      auth_srid = sl[1];
2111
    }
2112
    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)" )
2113
            .arg( srid )
2114
            .arg( QgsMssqlUtils::quotedValue( auth_name ), auth_srid, QgsMssqlUtils::quotedValue( srs.toWkt() ), QgsMssqlUtils::quotedValue( srs.toProj() ) );
2115

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

    
2118
    if ( !q.exec( sql ) )
2119
    {
2120
      logWrapper->setError( q.lastError().text() );
2121
      if ( errorMessage )
2122
        *errorMessage = q.lastError().text();
2123
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2124
    }
2125
  }
2126

    
2127
  // get wkb type and dimension
2128
  QString geometryType;
2129
  int dim = 2;
2130
  mssqlWkbTypeAndDimension( wkbType, geometryType, dim );
2131

    
2132
  if ( overwrite )
2133
  {
2134
    // remove the old table with the same name
2135
    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;" )
2136
            .arg( schemaName, tableName );
2137

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

    
2140
    if ( !q.exec( sql ) )
2141
    {
2142
      logWrapper->setError( q.lastError().text() );
2143
      if ( errorMessage )
2144
        *errorMessage = q.lastError().text();
2145
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2146
    }
2147
  }
2148
  else
2149
  {
2150
    // test for existing
2151
    sql = QStringLiteral( "SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')" )
2152
            .arg( schemaName, tableName );
2153

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

    
2156
    if ( !q.exec( sql ) )
2157
    {
2158
      logWrapper->setError( q.lastError().text() );
2159
      if ( errorMessage )
2160
        *errorMessage = q.lastError().text();
2161
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2162
    }
2163

    
2164
    // if we got a hit, abort!!
2165
    if ( q.next() )
2166
    {
2167
      if ( errorMessage )
2168
        *errorMessage = tr( "Table [%1].[%2] already exists" ).arg( schemaName, tableName );
2169
      return Qgis::VectorExportResult::ErrorCreatingLayer;
2170
    }
2171
  }
2172

    
2173
  if ( !geometryColumn.isEmpty() )
2174
  {
2175
    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"
2176
                          "CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL, [%4] [geometry] NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
2177
                          "DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
2178
                          "INSERT INTO [geometry_columns] ([f_table_catalog], [f_table_schema],[f_table_name], "
2179
                          "[f_geometry_column],[coord_dimension],[srid],[geometry_type]) VALUES ('%5', '%1', '%2', '%4', %6, %7, '%8')" )
2180
            .arg( schemaName, tableName, primaryKey, geometryColumn, dbName, QString::number( dim ), QString::number( srid ), geometryType );
2181
  }
2182
  else
2183
  {
2184
    //geometryless table
2185
    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"
2186
                          "CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
2187
                          "DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
2188
    )
2189
            .arg( schemaName, tableName, primaryKey );
2190
  }
2191

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

    
2194
  if ( !q.exec( sql ) )
2195
  {
2196
    logWrapper->setError( q.lastError().text() );
2197
    if ( errorMessage )
2198
      *errorMessage = q.lastError().text();
2199
    return Qgis::VectorExportResult::ErrorCreatingLayer;
2200
  }
2201

    
2202
  // clear any resources hold by the query
2203
  q.clear();
2204
  q.setForwardOnly( true );
2205

    
2206
  // use the provider to edit the table
2207
  dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
2208

    
2209
  const QgsDataProvider::ProviderOptions providerOptions;
2210
  const Qgis::DataProviderReadFlags flags;
2211
  auto provider = std::make_unique< QgsMssqlProvider >( dsUri.uri(), providerOptions, flags );
2212
  if ( !provider->isValid() )
2213
  {
2214
    if ( errorMessage )
2215
      *errorMessage = QObject::tr( "Loading of the MSSQL provider failed" );
2216

    
2217
    return Qgis::VectorExportResult::ErrorInvalidLayer;
2218
  }
2219

    
2220
  // add fields to the layer
2221
  if ( oldToNewAttrIdxMap )
2222
    oldToNewAttrIdxMap->clear();
2223

    
2224
  if ( fields.size() > 0 )
2225
  {
2226
    const QgsFields providerFields = provider->fields();
2227
    int offset = providerFields.size();
2228

    
2229
    // get the list of fields
2230
    QList<QgsField> flist;
2231
    for ( int originalFieldIndex = 0, n = fields.size(); originalFieldIndex < n; ++originalFieldIndex )
2232
    {
2233
      QgsField field = fields.at( originalFieldIndex );
2234
      if ( field.name() == geometryColumn )
2235
      {
2236
        // Found a field with the same name of the geometry column. Skip it!
2237
        continue;
2238
      }
2239

    
2240
      const int providerIndex = providerFields.lookupField( field.name() );
2241

    
2242
      if ( providerIndex >= 0 )
2243
      {
2244
        // we've already created this field (i.e. it was set in the CREATE TABLE statement), so
2245
        // we don't need to re-add it now
2246
        if ( oldToNewAttrIdxMap )
2247
          oldToNewAttrIdxMap->insert( originalFieldIndex, providerIndex );
2248
        continue;
2249
      }
2250

    
2251
      if ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( field ) )
2252
      {
2253
        if ( errorMessage )
2254
          *errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( field.name() );
2255

    
2256
        return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
2257
      }
2258

    
2259
      flist.append( field );
2260
      if ( oldToNewAttrIdxMap )
2261
        oldToNewAttrIdxMap->insert( originalFieldIndex, offset++ );
2262
    }
2263

    
2264
    if ( !provider->addAttributes( flist ) )
2265
    {
2266
      if ( errorMessage )
2267
        *errorMessage = QObject::tr( "Creation of fields failed" );
2268

    
2269
      return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
2270
    }
2271
  }
2272
  return Qgis::VectorExportResult::Success;
2273
}
2274

    
2275

    
2276
/**
2277
 * Class factory to return a pointer to a newly created
2278
 * QgsMssqlProvider object
2279
 */
2280
QgsMssqlProvider *QgsMssqlProviderMetadata::createProvider(
2281
  const QString &uri,
2282
  const QgsDataProvider::ProviderOptions &options,
2283
  Qgis::DataProviderReadFlags flags
2284
)
2285
{
2286
  return new QgsMssqlProvider( uri, options, flags );
2287
}
2288

    
2289
QList<QgsDataItemProvider *> QgsMssqlProviderMetadata::dataItemProviders() const
2290
{
2291
  QList<QgsDataItemProvider *> providers;
2292
  providers << new QgsMssqlDataItemProvider;
2293
  return providers;
2294
}
2295

    
2296
QgsTransaction *QgsMssqlProviderMetadata::createTransaction( const QString &connString )
2297
{
2298
  return new QgsMssqlTransaction( connString );
2299
}
2300

    
2301
QMap<QString, QgsAbstractProviderConnection *> QgsMssqlProviderMetadata::connections( bool cached )
2302
{
2303
  return connectionsProtected<QgsMssqlProviderConnection, QgsMssqlConnection>( cached );
2304
}
2305

    
2306
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &name )
2307
{
2308
  return new QgsMssqlProviderConnection( name );
2309
}
2310

    
2311
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
2312
{
2313
  return new QgsMssqlProviderConnection( uri, configuration );
2314
}
2315

    
2316
void QgsMssqlProviderMetadata::deleteConnection( const QString &name )
2317
{
2318
  deleteConnectionProtected<QgsMssqlProviderConnection>( name );
2319
}
2320

    
2321
void QgsMssqlProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name )
2322
{
2323
  saveConnectionProtected( conn, name );
2324
}
2325

    
2326
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, QString &createdLayerUri )
2327
{
2328
  return QgsMssqlProvider::createEmptyLayer(
2329
    uri, fields, wkbType, srs, overwrite,
2330
    &oldToNewAttrIdxMap, createdLayerUri, &errorMessage, options
2331
  );
2332
}
2333

    
2334

    
2335
QString buildfTableCatalogClause( const QgsDataSourceUri &dsUri )
2336
{
2337
  return QStringLiteral( "f_table_catalog%1" ).arg( dsUri.database().isEmpty() ? QStringLiteral( " IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsMssqlUtils::quotedValue( dsUri.database() ) ) );
2338
}
2339

    
2340

    
2341
bool QgsMssqlProviderMetadata::styleExists( const QString &uri, const QString &styleId, QString &errorCause )
2342
{
2343
  errorCause.clear();
2344
  const QgsDataSourceUri dsUri( uri );
2345
  // connect to database
2346
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2347

    
2348
  if ( !db->isValid() )
2349
  {
2350
    errorCause = QObject::tr( "Error connecting to database: %1" ).arg( db->errorText() );
2351
    return false;
2352
  }
2353

    
2354
  QSqlQuery query = QSqlQuery( db->db() );
2355
  query.setForwardOnly( true );
2356
  const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'layer_styles'" ) };
2357

    
2358
  if ( !LoggedExecMetadata( query, sql, uri ) )
2359
  {
2360
    errorCause = QObject::tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2361
    return false;
2362
  }
2363
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2364
  {
2365
    // no layer_styles table
2366
    query.finish();
2367
    return false;
2368
  }
2369

    
2370
  query.clear();
2371
  query.setForwardOnly( true );
2372
  const QString checkQuery = QString( "SELECT styleName"
2373
                                      " FROM layer_styles"
2374
                                      " WHERE %1"
2375
                                      " AND f_table_schema=%2"
2376
                                      " AND f_table_name=%3"
2377
                                      " AND f_geometry_column=%4"
2378
                                      " AND styleName=%5" )
2379
                               .arg( buildfTableCatalogClause( dsUri ) )
2380
                               .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2381
                               .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2382
                               .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
2383
                               .arg( QgsMssqlUtils::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) );
2384

    
2385
  if ( !LoggedExecMetadata( query, checkQuery, uri ) )
2386
  {
2387
    errorCause = QObject::tr( "Checking for style failed: %1" ).arg( query.lastError().text() );
2388
    return false;
2389
  }
2390

    
2391
  if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleId )
2392
  {
2393
    return true;
2394
  }
2395
  else
2396
  {
2397
    return false;
2398
  }
2399
}
2400

    
2401
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 )
2402
{
2403
  const QgsDataSourceUri dsUri( uri );
2404
  // connect to database
2405
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2406

    
2407
  if ( !db->isValid() )
2408
  {
2409
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2410
    QgsDebugError( db->errorText() );
2411
    return false;
2412
  }
2413

    
2414
  QSqlQuery query = QSqlQuery( db->db() );
2415
  query.setForwardOnly( true );
2416

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

    
2419
  if ( !LoggedExecMetadata( query, sql, uri ) )
2420
  {
2421
    QgsDebugError( query.lastError().text() );
2422
    return false;
2423
  }
2424
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2425
  {
2426
    QgsDebugMsgLevel( QStringLiteral( "Need to create styles table" ), 2 );
2427

    
2428
    sql = QStringLiteral( "CREATE TABLE [dbo].[layer_styles]("
2429
                          "[id] int IDENTITY(1,1) PRIMARY KEY,"
2430
                          "[f_table_catalog] [varchar](1024) NULL,"
2431
                          "[f_table_schema] [varchar](1024) NULL,"
2432
                          "[f_table_name] [varchar](1024) NULL,"
2433
                          "[f_geometry_column] [varchar](1024) NULL,"
2434
                          "[styleName] [varchar](1024) NULL,"
2435
                          "[styleQML] [text] NULL,"
2436
                          "[styleSLD] [text] NULL,"
2437
                          "[useAsDefault] [int] NULL,"
2438
                          "[description] [text] NULL,"
2439
                          "[owner] [varchar](1024) NULL,"
2440
                          "[ui] [text] NULL,"
2441
                          "[update_time] [datetime] NULL"
2442
                          ") ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]" );
2443

    
2444
    const bool execOk = LoggedExecMetadata( query, sql, uri );
2445
    if ( !execOk )
2446
    {
2447
      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" ) };
2448
      errCause = error;
2449
      return false;
2450
    }
2451
    query.finish();
2452
    query.clear();
2453
    query.setForwardOnly( true );
2454
  }
2455

    
2456
  QString uiFileColumn;
2457
  QString uiFileValue;
2458
  if ( !uiFileContent.isEmpty() )
2459
  {
2460
    uiFileColumn = QStringLiteral( ",ui" );
2461
    uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( uiFileContent );
2462
  }
2463
  QgsDebugMsgLevel( QStringLiteral( "Ready to insert new style" ), 2 );
2464
  // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values
2465
  // can contain user entered strings, which may themselves include %## values that would be
2466
  // replaced by the QString.arg function.  To ensure that the final SQL string is not corrupt these
2467
  // two values are both replaced in the final .arg call of the string construction.
2468

    
2469
  sql = QStringLiteral( "INSERT INTO layer_styles"
2470
                        "(f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner%11"
2471
                        ") VALUES ("
2472
                        "%1,%2,%3,%4,%5,%6,%7,%8,%9,%10%12"
2473
                        ")" )
2474
          .arg( QgsMssqlUtils::quotedValue( dsUri.database() ) )
2475
          .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2476
          .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2477
          .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
2478
          .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
2479
          .arg( QgsMssqlUtils::quotedValue( qmlStyle ) )
2480
          .arg( QgsMssqlUtils::quotedValue( sldStyle ) )
2481
          .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
2482
          .arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
2483
          .arg( QgsMssqlUtils::quotedValue( dsUri.username() ) )
2484
          .arg( uiFileColumn )
2485
          .arg( uiFileValue );
2486

    
2487
  const QString checkQuery = QStringLiteral( "SELECT styleName"
2488
                                             " FROM layer_styles"
2489
                                             " WHERE %1"
2490
                                             " AND f_table_schema=%2"
2491
                                             " AND f_table_name=%3"
2492
                                             " AND f_geometry_column=%4"
2493
                                             " AND styleName=%5" )
2494
                               .arg( buildfTableCatalogClause( dsUri ) )
2495
                               .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2496
                               .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2497
                               .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
2498
                               .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
2499

    
2500
  if ( !LoggedExecMetadata( query, checkQuery, uri ) )
2501
  {
2502
    QgsDebugError( query.lastError().text() );
2503
    QgsDebugError( QStringLiteral( "Check Query failed" ) );
2504
    return false;
2505
  }
2506
  if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleName )
2507
  {
2508
    QgsDebugMsgLevel( QStringLiteral( "Updating styles" ), 2 );
2509
    sql = QString( "UPDATE layer_styles "
2510
                   " SET useAsDefault=%1"
2511
                   ",styleQML=%2"
2512
                   ",styleSLD=%3"
2513
                   ",description=%4"
2514
                   ",owner=%5"
2515
                   " WHERE %6"
2516
                   " AND f_table_schema=%7"
2517
                   " AND f_table_name=%8"
2518
                   " AND f_geometry_column=%9"
2519
                   " AND styleName=%10" )
2520
            .arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
2521
            .arg( QgsMssqlUtils::quotedValue( qmlStyle ) )
2522
            .arg( QgsMssqlUtils::quotedValue( sldStyle ) )
2523
            .arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
2524
            .arg( QgsMssqlUtils::quotedValue( dsUri.username() ) )
2525
            .arg( buildfTableCatalogClause( dsUri ) )
2526
            .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2527
            .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2528
            .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
2529
            .arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
2530
  }
2531
  if ( useAsDefault )
2532
  {
2533
    const QString removeDefaultSql = QString( "UPDATE layer_styles "
2534
                                              " SET useAsDefault=0"
2535
                                              " WHERE %1"
2536
                                              " AND f_table_schema=%2"
2537
                                              " AND f_table_name=%3"
2538
                                              " AND f_geometry_column=%4" )
2539
                                       .arg( buildfTableCatalogClause( dsUri ) )
2540
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2541
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2542
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
2543
    sql = QStringLiteral( "%1; %2;" ).arg( removeDefaultSql, sql );
2544
  }
2545

    
2546
  QgsDebugMsgLevel( QStringLiteral( "Inserting styles" ), 2 );
2547
  QgsDebugMsgLevel( sql, 2 );
2548

    
2549
  const bool execOk = LoggedExecMetadata( query, sql, uri );
2550

    
2551
  if ( !execOk )
2552
  {
2553
    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." );
2554
  }
2555
  return execOk;
2556
}
2557

    
2558
QString QgsMssqlProviderMetadata::loadStyle( const QString &uri, QString &errCause )
2559
{
2560
  QString styleName;
2561
  return loadStoredStyle( uri, styleName, errCause );
2562
}
2563

    
2564
QString QgsMssqlProviderMetadata::loadStoredStyle( const QString &uri, QString &styleName, QString &errCause )
2565
{
2566
  errCause.clear();
2567
  const QgsDataSourceUri dsUri( uri );
2568
  // connect to database
2569
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2570

    
2571
  if ( !db->isValid() )
2572
  {
2573
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2574
    QgsDebugError( db->errorText() );
2575
    errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
2576
    return QString();
2577
  }
2578

    
2579
  QSqlQuery query = QSqlQuery( db->db() );
2580

    
2581
  query.setForwardOnly( true );
2582

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

    
2585
  if ( !LoggedExecMetadata( query, sql, uri ) )
2586
  {
2587
    errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2588
    return QString();
2589
  }
2590
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2591
  {
2592
    // no layer_styles table
2593
    errCause = tr( "Style does not exist" );
2594
    query.finish();
2595
    return QString();
2596
  }
2597

    
2598
  query.clear();
2599
  query.setForwardOnly( true );
2600

    
2601
  const QString selectQmlQuery = QString( "SELECT top 1 styleName, styleQML"
2602
                                          " FROM layer_styles"
2603
                                          " WHERE %1"
2604
                                          " AND f_table_schema=%2"
2605
                                          " AND f_table_name=%3"
2606
                                          " AND f_geometry_column=%4"
2607
                                          " ORDER BY useAsDefault desc" )
2608
                                   .arg( buildfTableCatalogClause( dsUri ) )
2609
                                   .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2610
                                   .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2611
                                   .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
2612

    
2613
  if ( !LoggedExecMetadata( query, selectQmlQuery, uri ) )
2614
  {
2615
    QgsDebugMsgLevel( QStringLiteral( "Load of style failed" ), 2 );
2616
    const QString msg = query.lastError().text();
2617
    errCause = msg;
2618
    QgsDebugError( msg );
2619
    return QString();
2620
  }
2621
  if ( query.isActive() && query.next() )
2622
  {
2623
    styleName = query.value( 0 ).toString();
2624
    const QString style = query.value( 1 ).toString();
2625
    return style;
2626
  }
2627
  return QString();
2628
}
2629

    
2630
int QgsMssqlProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause )
2631
{
2632
  const QgsDataSourceUri dsUri( uri );
2633
  // connect to database
2634
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2635

    
2636
  if ( !db->isValid() )
2637
  {
2638
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2639
    QgsDebugError( db->errorText() );
2640
    return -1;
2641
  }
2642

    
2643
  QSqlQuery query = QSqlQuery( db->db() );
2644
  query.setForwardOnly( true );
2645

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

    
2648
  // check if layer_styles table already exist
2649
  if ( !LoggedExecMetadata( query, sql, uri ) )
2650
  {
2651
    const QString msg = query.lastError().text();
2652
    errCause = msg;
2653
    QgsDebugError( msg );
2654
    return -1;
2655
  }
2656
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2657
  {
2658
    QgsDebugMsgLevel( QStringLiteral( "No styles available on DB" ), 2 );
2659
    return -1;
2660
  }
2661

    
2662
  const QString fTableCatalogClause = buildfTableCatalogClause( dsUri );
2663

    
2664
  const QString selectRelatedQuery = QString( "SELECT id,styleName,description"
2665
                                              " FROM layer_styles "
2666
                                              " WHERE %1"
2667
                                              " AND f_table_schema=%2"
2668
                                              " AND f_table_name=%3"
2669
                                              " AND f_geometry_column=%4"
2670
                                              " ORDER BY useasdefault DESC, update_time DESC" )
2671
                                       .arg( fTableCatalogClause )
2672
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2673
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2674
                                       .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
2675

    
2676

    
2677
  bool queryOk = LoggedExecMetadata( query, selectRelatedQuery, uri );
2678
  if ( !queryOk )
2679
  {
2680
    QgsDebugError( query.lastError().text() );
2681
    return -1;
2682
  }
2683
  int numberOfRelatedStyles = 0;
2684
  while ( query.isActive() && query.next() )
2685
  {
2686
    QgsDebugMsgLevel( query.value( 1 ).toString(), 2 );
2687
    ids.append( query.value( 0 ).toString() );
2688
    names.append( query.value( 1 ).toString() );
2689
    descriptions.append( query.value( 2 ).toString() );
2690
    numberOfRelatedStyles = numberOfRelatedStyles + 1;
2691
  }
2692
  const QString selectOthersQuery = QString( "SELECT id,styleName,description"
2693
                                             " FROM layer_styles "
2694
                                             " WHERE NOT (%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)"
2695
                                             " ORDER BY update_time DESC" )
2696
                                      .arg( fTableCatalogClause )
2697
                                      .arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
2698
                                      .arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
2699
                                      .arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
2700
  QgsDebugMsgLevel( selectOthersQuery, 2 );
2701

    
2702
  queryOk = LoggedExecMetadata( query, selectOthersQuery, uri );
2703
  if ( !queryOk )
2704
  {
2705
    QgsDebugError( query.lastError().text() );
2706
    return -1;
2707
  }
2708
  while ( query.next() )
2709
  {
2710
    ids.append( query.value( 0 ).toString() );
2711
    names.append( query.value( 1 ).toString() );
2712
    descriptions.append( query.value( 2 ).toString() );
2713
  }
2714
  return numberOfRelatedStyles;
2715
}
2716

    
2717
QgsMssqlProviderMetadata::QgsMssqlProviderMetadata()
2718
  : QgsProviderMetadata( QgsMssqlProvider::MSSQL_PROVIDER_KEY, QgsMssqlProvider::MSSQL_PROVIDER_DESCRIPTION )
2719
{
2720
}
2721

    
2722
QIcon QgsMssqlProviderMetadata::icon() const
2723
{
2724
  return QgsApplication::getThemeIcon( QStringLiteral( "mIconMssql.svg" ) );
2725
}
2726

    
2727
QString QgsMssqlProviderMetadata::getStyleById( const QString &uri, const QString &styleId, QString &errCause )
2728
{
2729
  const QgsDataSourceUri dsUri( uri );
2730
  // connect to database
2731
  std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
2732

    
2733
  if ( !db->isValid() )
2734
  {
2735
    QgsDebugError( QStringLiteral( "Error connecting to database" ) );
2736
    QgsDebugError( db->errorText() );
2737
    errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
2738
    return QString();
2739
  }
2740

    
2741
  QSqlQuery query = QSqlQuery( db->db() );
2742
  query.setForwardOnly( true );
2743

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

    
2746
  if ( !LoggedExecMetadata( query, sql, uri ) )
2747
  {
2748
    errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
2749
    return QString();
2750
  }
2751
  if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
2752
  {
2753
    // no layer_styles table
2754
    errCause = tr( "Style does not exist" );
2755
    query.finish();
2756
    return QString();
2757
  }
2758

    
2759
  query.clear();
2760
  query.setForwardOnly( true );
2761

    
2762
  QString style;
2763
  const QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsMssqlUtils::quotedValue( styleId ) );
2764

    
2765
  const bool queryOk = LoggedExecMetadata( query, selectQmlQuery, uri );
2766
  if ( !queryOk )
2767
  {
2768
    QgsDebugError( query.lastError().text() );
2769
    errCause = query.lastError().text();
2770
    return QString();
2771
  }
2772
  if ( !query.next() )
2773
  {
2774
    errCause = tr( "Style does not exist" );
2775
  }
2776
  else
2777
  {
2778
    style = query.value( 0 ).toString();
2779
  }
2780
  return style;
2781
}
2782

    
2783
QVariantMap QgsMssqlProviderMetadata::decodeUri( const QString &uri ) const
2784
{
2785
  const QgsDataSourceUri dsUri { uri };
2786
  QVariantMap uriParts;
2787

    
2788
  if ( !dsUri.database().isEmpty() )
2789
    uriParts[QStringLiteral( "dbname" )] = dsUri.database();
2790
  if ( !dsUri.host().isEmpty() )
2791
    uriParts[QStringLiteral( "host" )] = dsUri.host();
2792
  if ( !dsUri.port().isEmpty() )
2793
    uriParts[QStringLiteral( "port" )] = dsUri.port();
2794
  if ( !dsUri.service().isEmpty() )
2795
    uriParts[QStringLiteral( "service" )] = dsUri.service();
2796
  if ( !dsUri.username().isEmpty() )
2797
    uriParts[QStringLiteral( "username" )] = dsUri.username();
2798
  if ( !dsUri.password().isEmpty() )
2799
    uriParts[QStringLiteral( "password" )] = dsUri.password();
2800

    
2801
  // Supported?
2802
  //if ( ! dsUri.authConfigId().isEmpty() )
2803
  //  uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId();
2804

    
2805
  if ( dsUri.wkbType() != Qgis::WkbType::Unknown )
2806
    uriParts[QStringLiteral( "type" )] = static_cast<quint32>( dsUri.wkbType() );
2807

    
2808
  // Supported?
2809
  // uriParts[ QStringLiteral( "selectatid" ) ] = dsUri.selectAtIdDisabled();
2810

    
2811
  if ( !dsUri.table().isEmpty() )
2812
    uriParts[QStringLiteral( "table" )] = dsUri.table();
2813
  if ( !dsUri.schema().isEmpty() )
2814
    uriParts[QStringLiteral( "schema" )] = dsUri.schema();
2815
  if ( !dsUri.keyColumn().isEmpty() )
2816
    uriParts[QStringLiteral( "key" )] = dsUri.keyColumn();
2817
  if ( !dsUri.srid().isEmpty() )
2818
    uriParts[QStringLiteral( "srid" )] = dsUri.srid();
2819

    
2820
  uriParts[QStringLiteral( "estimatedmetadata" )] = dsUri.useEstimatedMetadata();
2821

    
2822
  // is this supported?
2823
  // uriParts[ QStringLiteral( "sslmode" ) ] = dsUri.sslMode();
2824

    
2825
  if ( !dsUri.sql().isEmpty() )
2826
    uriParts[QStringLiteral( "sql" )] = dsUri.sql();
2827
  if ( !dsUri.geometryColumn().isEmpty() )
2828
    uriParts[QStringLiteral( "geometrycolumn" )] = dsUri.geometryColumn();
2829

    
2830
  // From configuration
2831
  static const QStringList configurationParameters {
2832
    QStringLiteral( "geometryColumnsOnly" ),
2833
    QStringLiteral( "allowGeometrylessTables" ),
2834
    QStringLiteral( "saveUsername" ),
2835
    QStringLiteral( "savePassword" ),
2836
    QStringLiteral( "estimatedMetadata" ),
2837
    QStringLiteral( "disableInvalidGeometryHandling" ),
2838
    QStringLiteral( "extentInGeometryColumns" ),
2839
    QStringLiteral( "primaryKeyInGeometryColumns" )
2840
  };
2841

    
2842
  for ( const auto &configParam : configurationParameters )
2843
  {
2844
    if ( dsUri.hasParam( configParam ) )
2845
    {
2846
      uriParts[configParam] = dsUri.param( configParam );
2847
    }
2848
  }
2849

    
2850
  return uriParts;
2851
}
2852

    
2853
QString QgsMssqlProviderMetadata::encodeUri( const QVariantMap &parts ) const
2854
{
2855
  QgsDataSourceUri dsUri;
2856
  if ( parts.contains( QStringLiteral( "dbname" ) ) )
2857
    dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
2858
  // Also accepts "database"
2859
  if ( parts.contains( QStringLiteral( "database" ) ) )
2860
    dsUri.setDatabase( parts.value( QStringLiteral( "database" ) ).toString() );
2861
  // Supported?
2862
  //if ( parts.contains( QStringLiteral( "port" ) ) )
2863
  //  dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
2864
  if ( parts.contains( QStringLiteral( "host" ) ) )
2865
    dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
2866
  if ( parts.contains( QStringLiteral( "service" ) ) )
2867
    dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
2868
  if ( parts.contains( QStringLiteral( "username" ) ) )
2869
    dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
2870
  if ( parts.contains( QStringLiteral( "password" ) ) )
2871
    dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
2872
  // Supported?
2873
  //if ( parts.contains( QStringLiteral( "authcfg" ) ) )
2874
  //  dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
2875
  if ( parts.contains( QStringLiteral( "type" ) ) )
2876
    dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<Qgis::WkbType>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
2877
  // Supported?
2878
  //if ( parts.contains( QStringLiteral( "selectatid" ) ) )
2879
  //  dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
2880
  if ( parts.contains( QStringLiteral( "table" ) ) )
2881
    dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
2882
  if ( parts.contains( QStringLiteral( "schema" ) ) )
2883
    dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
2884
  if ( parts.contains( QStringLiteral( "key" ) ) )
2885
    dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
2886
  if ( parts.contains( QStringLiteral( "srid" ) ) )
2887
    dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
2888
  if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
2889
    dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
2890
  // Supported?
2891
  //if ( parts.contains( QStringLiteral( "sslmode" ) ) )
2892
  //  dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt( ) ) ) );
2893
  if ( parts.contains( QStringLiteral( "sql" ) ) )
2894
    dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
2895
  // Supported?
2896
  //if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
2897
  //  dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
2898
  if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
2899
    dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
2900
  if ( parts.contains( QStringLiteral( "disableInvalidGeometryHandling" ) ) )
2901
    dsUri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), parts.value( QStringLiteral( "disableInvalidGeometryHandling" ) ).toString() );
2902
  if ( parts.contains( QStringLiteral( "allowGeometrylessTables" ) ) )
2903
    dsUri.setParam( QStringLiteral( "allowGeometrylessTables" ), parts.value( QStringLiteral( "allowGeometrylessTables" ) ).toString() );
2904
  if ( parts.contains( QStringLiteral( "geometryColumnsOnly" ) ) )
2905
    dsUri.setParam( QStringLiteral( "geometryColumnsOnly" ), parts.value( QStringLiteral( "geometryColumnsOnly" ) ).toString() );
2906
  if ( parts.contains( QStringLiteral( "extentInGeometryColumns" ) ) )
2907
    dsUri.setParam( QStringLiteral( "extentInGeometryColumns" ), parts.value( QStringLiteral( "extentInGeometryColumns" ) ).toString() );
2908
  if ( parts.contains( QStringLiteral( "primaryKeyInGeometryColumns" ) ) )
2909
    dsUri.setParam( QStringLiteral( "primaryKeyInGeometryColumns" ), parts.value( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toString() );
2910
  return dsUri.uri();
2911
}
2912

    
2913
QList<Qgis::LayerType> QgsMssqlProviderMetadata::supportedLayerTypes() const
2914
{
2915
  return { Qgis::LayerType::Vector };
2916
}
2917

    
2918
QString QgsMssqlProvider::typeFromMetadata( const QString &typeName, int numCoords )
2919
{
2920
  QString type { typeName };
2921
  const bool hasM { typeName.endsWith( 'M', Qt::CaseInsensitive ) };
2922
  if ( numCoords == 4 )
2923
  {
2924
    if ( hasM )
2925
    {
2926
      type.chop( 1 );
2927
    }
2928
    type.append( QStringLiteral( "ZM" ) );
2929
  }
2930
  else if ( numCoords == 3 )
2931
  {
2932
    if ( !hasM )
2933
    {
2934
      type.append( QStringLiteral( "Z" ) );
2935
    }
2936
  }
2937
  return type;
2938
}
2939

    
2940
bool QgsMssqlProviderMetadata::execLogged( QSqlQuery &qry, const QString &sql, const QString &uri, const QString &queryOrigin ) const
2941
{
2942
  QgsDatabaseQueryLogWrapper logWrapper { sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProviderMetadata" ), queryOrigin };
2943
  const bool res { qry.exec( sql ) };
2944
  if ( !res )
2945
  {
2946
    logWrapper.setError( qry.lastError().text() );
2947
  }
2948
  else
2949
  {
2950
    if ( qry.isSelect() )
2951
    {
2952
      logWrapper.setFetchedRows( qry.size() );
2953
    }
2954
    else
2955
    {
2956
      logWrapper.setFetchedRows( qry.numRowsAffected() );
2957
    }
2958
  }
2959
  logWrapper.setQuery( qry.lastQuery() );
2960
  return res;
2961
}
2962

    
2963
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
2964
{
2965
  return new QgsMssqlProviderMetadata();
2966
}
2967

    
2968
// ----------
2969

    
2970
QgsFeatureId QgsMssqlSharedData::lookupFid( const QVariantList &v )
2971
{
2972
  const QMutexLocker locker( &mMutex );
2973

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

    
2976
  if ( it != mKeyToFid.constEnd() )
2977
  {
2978
    return it.value();
2979
  }
2980

    
2981
  mFidToKey.insert( ++mFidCounter, v );
2982
  mKeyToFid.insert( v, mFidCounter );
2983

    
2984
  return mFidCounter;
2985
}
2986

    
2987
QVariant QgsMssqlSharedData::removeFid( QgsFeatureId fid )
2988
{
2989
  const QMutexLocker locker( &mMutex );
2990

    
2991
  const QVariantList v = mFidToKey[fid];
2992
  mFidToKey.remove( fid );
2993
  mKeyToFid.remove( v );
2994
  return v;
2995
}
2996

    
2997
void QgsMssqlSharedData::insertFid( QgsFeatureId fid, const QVariantList &k )
2998
{
2999
  const QMutexLocker locker( &mMutex );
3000

    
3001
  mFidToKey.insert( fid, k );
3002
  mKeyToFid.insert( k, fid );
3003
}
3004

    
3005
QVariantList QgsMssqlSharedData::lookupKey( QgsFeatureId featureId )
3006
{
3007
  const QMutexLocker locker( &mMutex );
3008

    
3009
  const QMap<QgsFeatureId, QVariantList>::const_iterator it = mFidToKey.find( featureId );
3010
  if ( it != mFidToKey.constEnd() )
3011
    return it.value();
3012
  return QVariantList();
3013
}
3014

    
3015
QString QgsMssqlProvider::whereClauseFid( QgsFeatureId featureId )
3016
{
3017
  QString whereClause;
3018

    
3019
  switch ( mPrimaryKeyType )
3020
  {
3021
    case QgsMssqlDatabase::PrimaryKeyType::Int:
3022
      Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
3023
      whereClause = QStringLiteral( "[%1]=%2" ).arg( mAttributeFields.at( mPrimaryKeyAttrs[0] ).name(), FID_TO_STRING( featureId ) );
3024
      break;
3025

    
3026
    case QgsMssqlDatabase::PrimaryKeyType::FidMap:
3027
    {
3028
      const QVariantList &pkVals = mShared->lookupKey( featureId );
3029
      if ( !pkVals.isEmpty() )
3030
      {
3031
        Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() );
3032

    
3033
        whereClause = QStringLiteral( "(" );
3034

    
3035
        QString delim;
3036
        for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
3037
        {
3038
          const QgsField &fld = mAttributeFields.at( mPrimaryKeyAttrs[i] );
3039
          whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), QgsMssqlUtils::quotedValue( pkVals[i] ) );
3040
          delim = QStringLiteral( " AND " );
3041
        }
3042

    
3043
        whereClause += QLatin1Char( ')' );
3044
      }
3045
      else
3046
      {
3047
        QgsDebugError( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) );
3048
        whereClause = QStringLiteral( "NULL IS NOT NULL" );
3049
      }
3050
    }
3051
    break;
3052

    
3053
    case QgsMssqlDatabase::PrimaryKeyType::Unknown:
3054
      Q_ASSERT( !"FAILURE: Primary key unknown" );
3055
      whereClause = QStringLiteral( "NULL IS NOT NULL" );
3056
      break;
3057
  }
3058

    
3059
  return whereClause;
3060
}
3061

    
3062
/* static */
3063
QStringList QgsMssqlProvider::parseUriKey( const QString &key )
3064
{
3065
  if ( key.isEmpty() )
3066
    return QStringList();
3067

    
3068
  QStringList cols;
3069

    
3070
  // remove quotes from key list
3071
  if ( key.startsWith( '"' ) && key.endsWith( '"' ) )
3072
  {
3073
    int i = 1;
3074
    QString col;
3075
    while ( i < key.size() )
3076
    {
3077
      if ( key[i] == '"' )
3078
      {
3079
        if ( i + 1 < key.size() && key[i + 1] == '"' )
3080
        {
3081
          i++;
3082
        }
3083
        else
3084
        {
3085
          cols << col;
3086
          col.clear();
3087

    
3088
          if ( ++i == key.size() )
3089
            break;
3090

    
3091
          Q_ASSERT( key[i] == ',' );
3092
          i++;
3093
          Q_ASSERT( key[i] == '"' );
3094
          i++;
3095
          col.clear();
3096
          continue;
3097
        }
3098
      }
3099

    
3100
      col += key[i++];
3101
    }
3102
  }
3103
  else if ( key.contains( ',' ) )
3104
  {
3105
    cols = key.split( ',' );
3106
  }
3107
  else
3108
  {
3109
    cols << key;
3110
  }
3111

    
3112
  return cols;
3113
}
3114

    
3115
bool QgsMssqlProvider::getExtentFromGeometryColumns( QgsRectangle &extent ) const
3116
{
3117
  QSqlQuery query = createQuery();
3118
  query.setForwardOnly( true );
3119

    
3120
  const QString sql = QStringLiteral( "SELECT qgis_xmin,qgis_xmax,qgis_ymin,qgis_ymax "
3121
                                      "FROM geometry_columns WHERE f_table_name = %1 AND f_table_schema = %2 "
3122
                                      "AND NOT (qgis_xmin IS NULL OR qgis_xmax IS NULL OR qgis_ymin IS NULL OR qgis_ymax IS NULL)" );
3123

    
3124
  const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) );
3125

    
3126
  if ( LoggedExec( query, statement ) && query.isActive() )
3127
  {
3128
    query.next();
3129
    if ( query.isValid() )
3130
    {
3131
      extent.setXMinimum( query.value( 0 ).toDouble() );
3132
      extent.setXMaximum( query.value( 1 ).toDouble() );
3133
      extent.setYMinimum( query.value( 2 ).toDouble() );
3134
      extent.setYMaximum( query.value( 3 ).toDouble() );
3135

    
3136
      return true;
3137
    }
3138
  }
3139

    
3140
  return false;
3141
}
3142

    
3143
bool QgsMssqlProvider::getPrimaryKeyFromGeometryColumns( QStringList &primaryKeys )
3144
{
3145
  QSqlQuery query = createQuery();
3146
  query.setForwardOnly( true );
3147
  primaryKeys.clear();
3148

    
3149
  const QString sql = QStringLiteral( "SELECT qgis_pkey FROM geometry_columns "
3150
                                      "WHERE f_table_name = %1 AND f_table_schema = %2 AND NOT qgis_pkey IS NULL" );
3151
  const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) );
3152

    
3153
  if ( LoggedExec( query, statement ) && query.isActive() )
3154
  {
3155
    query.next();
3156
    if ( query.isValid() )
3157
    {
3158
      primaryKeys = query.value( 0 ).toString().split( ',' );
3159
      if ( !primaryKeys.isEmpty() )
3160
        return true;
3161
    }
3162
  }
3163

    
3164
  return false;
3165
}