QGIS/src/providers/mssql/qgsmssqlprovider.cpp
Nyall Dawson da4260766f [mssql] Always use connection method with full QgsDataSourceUri
Remove the older method which took explicit host/database/service/...
arguments, and ensure we always use the method which takes a full
QgsDataSourceUri argument. This ensures that the db connection
always has access to ALL the properties of the original data source
uri, including any additional parameters set on it.
2025-08-16 05:19:02 +10:00

3173 lines
104 KiB
C++

/***************************************************************************
qgsmssqlprovider.cpp - Data provider for mssql server
-------------------
begin : 2011-10-08
copyright : (C) 2011 by Tamas Szekeres
email : szekerest at gmail.com
***************************************************************************/
/***************************************************************************
* *
* This program is free software; you can redistribute it and/or modify *
* it under the terms of the GNU General Public License as published by *
* the Free Software Foundation; either version 2 of the License, or *
* (at your option) any later version. *
* *
***************************************************************************/
#include "qgsmssqlprovider.h"
#include "moc_qgsmssqlprovider.cpp"
#include "qgsmssqlconnection.h"
#include "qgsmssqldatabase.h"
#include "qgsmssqlproviderconnection.h"
#include "qgsmssqlutils.h"
#include "qgsfeedback.h"
#include "qgsdbquerylog.h"
#include "qgsdbquerylog_p.h"
#include "qgsvariantutils.h"
#include "qgsthreadingutils.h"
#include <QtGlobal>
#include <QFileInfo>
#include <QDataStream>
#include <QStringList>
#include <QSettings>
#include <QRegularExpression>
#include <QUrl>
#include <QtSql/QSqlDatabase>
#include <QtSql/QSqlQuery>
#include <QtSql/QSqlError>
#include <QtSql/QSqlRecord>
#include <QtSql/QSqlField>
#include <QStringBuilder>
#include <QWaitCondition>
#include "qgsapplication.h"
#include "qgsdataprovider.h"
#include "qgsfeature.h"
#include "qgsfields.h"
#include "qgsgeometry.h"
#include "qgslogger.h"
#include "qgsmessagelog.h"
#include "qgsrectangle.h"
#include "qgis.h"
#include "qgsmssqldataitems.h"
#include "qgsmssqlfeatureiterator.h"
#include "qgsmssqltransaction.h"
#include "qgsconfig.h"
constexpr int sMssqlConQueryLogFilePrefixLength = CMAKE_SOURCE_DIR[sizeof( CMAKE_SOURCE_DIR ) - 1] == '/' ? sizeof( CMAKE_SOURCE_DIR ) + 1 : sizeof( CMAKE_SOURCE_DIR );
#define LoggedExec( query, sql ) execLogged( query, sql, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
#define LoggedExecPrepared( query ) execPreparedLogged( query, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
#define LoggedExecMetadata( query, sql, uri ) execLogged( query, sql, uri, QString( QString( __FILE__ ).mid( sMssqlConQueryLogFilePrefixLength ) + ':' + QString::number( __LINE__ ) + " (" + __FUNCTION__ + ")" ) )
const QString QgsMssqlProvider::MSSQL_PROVIDER_KEY = QStringLiteral( "mssql" );
const QString QgsMssqlProvider::MSSQL_PROVIDER_DESCRIPTION = QStringLiteral( "MSSQL spatial data provider" );
int QgsMssqlProvider::sConnectionId = 0;
QgsMssqlProvider::QgsMssqlProvider( const QString &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags )
: QgsVectorDataProvider( uri, options, flags )
, mUri( uri )
, mShared( new QgsMssqlSharedData )
{
if ( !mUri.srid().isEmpty() )
mSRId = mUri.srid().toInt();
else
mSRId = -1;
mWkbType = mUri.wkbType();
mValid = true;
mUseEstimatedMetadata = mUri.useEstimatedMetadata();
if ( mReadFlags & Qgis::DataProviderReadFlag::TrustDataSource )
{
mUseEstimatedMetadata = true;
}
mDisableInvalidGeometryHandling = mUri.hasParam( QStringLiteral( "disableInvalidGeometryHandling" ) )
? mUri.param( QStringLiteral( "disableInvalidGeometryHandling" ) ).toInt()
: false;
mUseGeometryColumnsTableForExtent = mUri.hasParam( QStringLiteral( "extentInGeometryColumns" ) )
? mUri.param( QStringLiteral( "extentInGeometryColumns" ) ).toInt()
: false;
mSqlWhereClause = mUri.sql();
mConn = QgsMssqlDatabase::connectDb( mUri, false );
if ( !mConn )
{
mValid = false;
return;
}
QSqlDatabase db = mConn->db();
if ( !db.isOpen() )
{
setLastError( db.lastError().text() );
QgsDebugError( mLastError );
mValid = false;
return;
}
// Database successfully opened; we can now issue SQL commands.
if ( mSchemaName.isEmpty() && mUri.table().startsWith( '(' ) && mUri.table().endsWith( ')' ) )
{
mIsQuery = true;
mQuery = mUri.table();
}
else
{
mIsQuery = false;
if ( !mUri.schema().isEmpty() )
mSchemaName = mUri.schema();
else
mSchemaName = QStringLiteral( "dbo" );
if ( !mUri.table().isEmpty() )
{
// the layer name has been specified
mTableName = mUri.table();
QStringList sl = mTableName.split( '.' );
if ( sl.length() == 2 )
{
mSchemaName = sl[0];
mTableName = sl[1];
}
mTables = QStringList( mTableName );
}
else
{
// Get a list of table
mTables = db.tables( QSql::Tables );
if ( !mTables.isEmpty() )
mTableName = mTables[0];
else
mValid = false;
}
}
if ( mValid )
{
if ( !mUri.geometryColumn().isEmpty() )
mGeometryColName = mUri.geometryColumn();
if ( !mIsQuery )
{
if ( mSRId < 0 || mWkbType == Qgis::WkbType::Unknown || mGeometryColName.isEmpty() )
{
loadMetadata();
}
else
{
// TODO query??
}
}
loadFields();
UpdateStatistics( mUseEstimatedMetadata );
//only for views, defined in layer data when loading layer for first time
bool primaryKeyFromGeometryColumnsTable = mUri.hasParam( QStringLiteral( "primaryKeyInGeometryColumns" ) )
? mUri.param( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toInt()
: false;
QStringList cols;
if ( primaryKeyFromGeometryColumnsTable )
{
mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Unknown;
mPrimaryKeyAttrs.clear();
primaryKeyFromGeometryColumnsTable = getPrimaryKeyFromGeometryColumns( cols );
if ( !primaryKeyFromGeometryColumnsTable )
QgsMessageLog::logMessage( tr( "Invalid primary key from geometry_columns table for layer '%1', get primary key from the layer." ).arg( mUri.table() ), tr( "MS SQL Server" ) );
}
if ( !primaryKeyFromGeometryColumnsTable )
{
const QString primaryKey = mUri.keyColumn();
if ( !primaryKey.isEmpty() )
{
mPrimaryKeyAttrs.clear();
cols = parseUriKey( primaryKey );
}
}
if ( mValid )
{
for ( const QString &col : std::as_const( cols ) )
{
const int idx = mAttributeFields.indexFromName( col );
if ( idx < 0 )
{
QgsMessageLog::logMessage( tr( "Key field '%1' for view/query not found." ).arg( col ), tr( "MSSQL" ) );
mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Unknown;
mPrimaryKeyAttrs.clear();
break;
}
const QgsField &fld = mAttributeFields.at( idx );
if ( mPrimaryKeyAttrs.size() == 0 && ( fld.type() == QMetaType::Type::Int || fld.type() == QMetaType::Type::LongLong || ( fld.type() == QMetaType::Type::Double && fld.precision() == 0 ) ) )
{
mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::Int;
}
else
{
mPrimaryKeyType = QgsMssqlDatabase::PrimaryKeyType::FidMap;
}
mPrimaryKeyAttrs << idx;
}
if ( mGeometryColName.isEmpty() )
{
// table contains no geometries
mWkbType = Qgis::WkbType::NoGeometry;
mSRId = 0;
}
}
}
if ( mValid && mIsQuery && mPrimaryKeyAttrs.isEmpty() )
{
const QString error = QStringLiteral( "No primary key could be found for query %1" ).arg( mQuery );
QgsDebugError( error );
mValid = false;
setLastError( error );
}
//fill type names into sets
setNativeTypes( QgsMssqlConnection::nativeTypes() );
}
QgsMssqlProvider::~QgsMssqlProvider()
{
}
QgsAbstractFeatureSource *QgsMssqlProvider::featureSource() const
{
return new QgsMssqlFeatureSource( this );
}
QgsFeatureIterator QgsMssqlProvider::getFeatures( const QgsFeatureRequest &request ) const
{
if ( !mValid )
{
QgsDebugError( QStringLiteral( "Read attempt on an invalid mssql data source" ) );
return QgsFeatureIterator();
}
return QgsFeatureIterator( new QgsMssqlFeatureIterator( new QgsMssqlFeatureSource( this ), true, request ) );
}
void QgsMssqlProvider::loadMetadata()
{
mSRId = 0;
mWkbType = Qgis::WkbType::Unknown;
QSqlQuery query = createQuery();
query.setForwardOnly( true );
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 ) ) ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( query.isActive() && query.next() )
{
mGeometryColName = query.value( 0 ).toString();
mSRId = query.value( 1 ).toInt();
const int dimensions = query.value( 3 ).toInt();
const QString detectedType { QgsMssqlProvider::typeFromMetadata( query.value( 2 ).toString().toUpper(), dimensions ) };
mWkbType = getWkbType( detectedType );
}
}
bool QgsMssqlProvider::execLogged( QSqlQuery &qry, const QString &sql, const QString &queryOrigin ) const
{
QgsDatabaseQueryLogWrapper logWrapper { sql, uri().uri(), QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), queryOrigin };
const bool res { qry.exec( sql ) };
if ( !res )
{
logWrapper.setError( qry.lastError().text() );
}
else
{
if ( qry.isSelect() )
{
logWrapper.setFetchedRows( qry.size() );
}
else
{
logWrapper.setFetchedRows( qry.numRowsAffected() );
}
}
logWrapper.setQuery( qry.lastQuery() );
return res;
}
bool QgsMssqlProvider::execPreparedLogged( QSqlQuery &qry, const QString &queryOrigin ) const
{
QgsDatabaseQueryLogWrapper logWrapper { qry.lastQuery(), uri().uri(), QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), queryOrigin };
const bool res { qry.exec() };
if ( !res )
{
logWrapper.setError( qry.lastError().text() );
}
else
{
if ( qry.isSelect() )
{
logWrapper.setFetchedRows( qry.size() );
}
else
{
logWrapper.setFetchedRows( qry.numRowsAffected() );
}
}
logWrapper.setQuery( qry.lastQuery() );
return res;
}
void QgsMssqlProvider::setLastError( const QString &error )
{
appendError( error );
mLastError = error;
}
QSqlQuery QgsMssqlProvider::createQuery() const
{
std::shared_ptr<QgsMssqlDatabase> conn = connection();
return conn->createQuery();
}
void QgsMssqlProvider::loadFields()
{
mAttributeFields.clear();
mDefaultValues.clear();
mComputedColumns.clear();
std::shared_ptr<QgsMssqlDatabase> conn = connection();
QgsMssqlDatabase::FieldDetails details;
details.geometryColumnName = mGeometryColName;
QString error;
const bool result = mIsQuery ? conn->loadQueryFields( details, mQuery, error )
: conn->loadFields( details, mSchemaName, mTableName, error );
if ( !result )
{
pushError( error );
return;
}
mComputedColumns = details.computedColumns;
mGeometryColName = details.geometryColumnName;
mGeometryColType = details.geometryColumnType;
mParser.mIsGeography = details.isGeography;
mPrimaryKeyType = details.primaryKeyType;
mPrimaryKeyAttrs = details.primaryKeyAttrs;
mAttributeFields = details.attributeFields;
mDefaultValues = details.defaultValues;
if ( !mIsQuery && mPrimaryKeyAttrs.isEmpty() )
{
const QString error = QStringLiteral( "No primary key could be found on table %1" ).arg( mTableName );
QgsDebugError( error );
mValid = false;
setLastError( error );
}
}
QString QgsMssqlProvider::defaultValueClause( int fieldId ) const
{
const QString defVal = mDefaultValues.value( fieldId, QString() );
if ( defVal.isEmpty() )
return QString();
// NOTE: If EvaluateDefaultValues is activated it is impossible to get the defaultValueClause.
// This also apply to QgsPostgresProvider::defaultValueClause.
if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
return defVal;
return QString();
}
QVariant QgsMssqlProvider::defaultValue( int fieldId ) const
{
const QString defVal = mDefaultValues.value( fieldId, QString() );
if ( defVal.isEmpty() )
return QVariant();
if ( !providerProperty( EvaluateDefaultValues, false ).toBool() )
return QVariant();
const QString sql = QStringLiteral( "select %1" )
.arg( defVal );
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
const QString errorMessage( tr( "Could not execute query: %1" ).arg( query.lastError().text() ) );
QgsDebugError( errorMessage );
pushError( errorMessage );
return QVariant();
}
if ( !query.next() )
{
const QString errorMessage( tr( "Could not fetch next query value: %1" ).arg( query.lastError().text() ) );
QgsDebugError( errorMessage );
pushError( errorMessage );
return QVariant();
}
const QVariant res = query.value( 0 );
return QgsVariantUtils::isNull( res ) ? QVariant() : res;
}
bool QgsMssqlProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint, const QVariant &value ) const
{
if ( providerProperty( EvaluateDefaultValues, false ).toBool() )
{
return !mDefaultValues.value( fieldIndex ).isEmpty();
}
else
{
// stricter check - if we are evaluating default values only on commit then we can only bypass the check
// if the attribute values matches the original default clause
return mDefaultValues.contains( fieldIndex ) && !mDefaultValues.value( fieldIndex ).isEmpty() && ( mDefaultValues.value( fieldIndex ) == value.toString() || QgsVariantUtils::isUnsetAttributeValue( value ) ) && !QgsVariantUtils::isNull( value );
}
}
QString QgsMssqlProvider::storageType() const
{
return QStringLiteral( "MSSQL spatial database" );
}
QVariant QgsMssqlProvider::convertTimeValue( const QVariant &value )
{
if ( value.isValid() && value.userType() == QMetaType::Type::QByteArray )
{
// time fields can be returned as byte arrays... woot
const QByteArray ba = value.toByteArray();
if ( ba.length() >= 5 )
{
const int hours = ba.at( 0 );
const int mins = ba.at( 2 );
const int seconds = ba.at( 4 );
QVariant t = QTime( hours, mins, seconds );
if ( !t.isValid() ) // can't handle it
t = QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
return t;
}
return QgsVariantUtils::createNullVariant( QMetaType::Type::QTime );
}
return value;
}
// Returns the minimum value of an attribute
QVariant QgsMssqlProvider::minimumValue( int index ) const
{
if ( index < 0 || index >= mAttributeFields.count() )
{
return QVariant();
}
// get the field name
const QgsField &fld = mAttributeFields.at( index );
QString sql = QStringLiteral( "SELECT min(%1) FROM " )
.arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
if ( mIsQuery )
{
sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
else
{
sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
if ( !mSqlWhereClause.isEmpty() )
{
sql += " WHERE (" + mSqlWhereClause + ')';
}
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( query.isActive() && query.next() )
{
QVariant v = query.value( 0 );
if ( fld.type() == QMetaType::Type::QTime )
v = convertTimeValue( v );
if ( v.userType() != fld.type() )
v = convertValue( fld.type(), v.toString() );
return v;
}
return QVariant();
}
// Returns the maximum value of an attribute
QVariant QgsMssqlProvider::maximumValue( int index ) const
{
if ( index < 0 || index >= mAttributeFields.count() )
{
return QVariant();
}
// get the field name
const QgsField &fld = mAttributeFields.at( index );
QString sql = QStringLiteral( "SELECT max(%1) FROM " )
.arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
if ( mIsQuery )
{
sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
else
{
sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
if ( !mSqlWhereClause.isEmpty() )
{
sql += " WHERE (" + mSqlWhereClause + ')';
}
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( query.isActive() && query.next() )
{
QVariant v = query.value( 0 );
if ( fld.type() == QMetaType::Type::QTime )
v = convertTimeValue( v );
if ( v.userType() != fld.type() )
v = convertValue( fld.type(), v.toString() );
return v;
}
return QVariant();
}
// Returns the list of unique values of an attribute
QSet<QVariant> QgsMssqlProvider::uniqueValues( int index, int limit ) const
{
QSet<QVariant> uniqueValues;
if ( index < 0 || index >= mAttributeFields.count() )
{
return uniqueValues;
}
// get the field name
const QgsField &fld = mAttributeFields.at( index );
QString sql = QStringLiteral( "SELECT DISTINCT " );
if ( limit > 0 )
{
sql += QStringLiteral( " TOP %1 " ).arg( limit );
}
sql += QStringLiteral( "%1 FROM " ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
if ( mIsQuery )
{
sql += QStringLiteral( " (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
else
{
sql += QStringLiteral( " %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
if ( !mSqlWhereClause.isEmpty() )
{
sql += " where (" + mSqlWhereClause + ')';
}
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( query.isActive() )
{
// read all features
while ( query.next() )
{
QVariant v = query.value( 0 );
if ( fld.type() == QMetaType::Type::QTime )
v = convertTimeValue( v );
if ( v.userType() != fld.type() )
v = convertValue( fld.type(), v.toString() );
uniqueValues.insert( v );
}
}
return uniqueValues;
}
QStringList QgsMssqlProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const
{
QStringList results;
if ( index < 0 || index >= mAttributeFields.count() )
{
return results;
}
// get the field name
const QgsField &fld = mAttributeFields.at( index );
QString sql = QStringLiteral( "SELECT DISTINCT " );
if ( limit > 0 )
{
sql += QStringLiteral( " TOP %1 " ).arg( limit );
}
sql += QStringLiteral( "%1 FROM " )
.arg( QgsMssqlUtils::quotedIdentifier( fld.name() ) );
if ( mIsQuery )
{
sql += QStringLiteral( " (%1) q WHERE %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause ) : QString() );
}
else
{
sql += QStringLiteral( " %1.%2 WHERE " ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
if ( !mSqlWhereClause.isEmpty() )
{
sql += QStringLiteral( " (%1) AND " ).arg( mSqlWhereClause );
}
}
sql += QStringLiteral( " %1 LIKE '%%2%'" ).arg( QgsMssqlUtils::quotedIdentifier( fld.name() ), substring );
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( query.isActive() )
{
// read all features
while ( query.next() )
{
results << query.value( 0 ).toString();
if ( feedback && feedback->isCanceled() )
break;
}
}
return results;
}
// update the extent, wkb type and srid for this layer, returns false if fails
void QgsMssqlProvider::UpdateStatistics( bool estimate ) const
{
if ( mGeometryColName.isEmpty() )
{
return;
}
// get features to calculate the statistics
QString statement;
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( mUseGeometryColumnsTableForExtent )
{
if ( !getExtentFromGeometryColumns( mExtent ) )
QgsMessageLog::logMessage( tr( "Invalid extent from geometry_columns table for layer '%1', get extent from the layer." ).arg( mTableName ), tr( "MSSQL" ) );
else
return;
}
if ( !mIsQuery )
{
// Get the extents from the spatial index table to speed up load times.
// 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.
const QString sql = "SELECT min(bounding_box_xmin), min(bounding_box_ymin), max(bounding_box_xmax), max(bounding_box_ymax)"
" FROM sys.spatial_index_tessellations WHERE object_id = OBJECT_ID('[%1].[%2]')";
statement = QString( sql ).arg( mSchemaName, mTableName );
if ( LoggedExec( query, statement ) )
{
if ( query.next() && ( !QgsVariantUtils::isNull( query.value( 0 ) ) || !QgsVariantUtils::isNull( query.value( 1 ) ) || !QgsVariantUtils::isNull( query.value( 2 ) ) || !QgsVariantUtils::isNull( query.value( 3 ) ) ) )
{
QgsDebugMsgLevel( QStringLiteral( "Found extents in spatial index" ), 2 );
mExtent.setXMinimum( query.value( 0 ).toDouble() );
mExtent.setYMinimum( query.value( 1 ).toDouble() );
mExtent.setXMaximum( query.value( 2 ).toDouble() );
mExtent.setYMaximum( query.value( 3 ).toDouble() );
return;
}
}
else
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
}
// If we can't find the extents in the spatial index table just do what we normally do.
bool readAllGeography = false;
if ( estimate )
{
if ( mGeometryColType == QLatin1String( "geometry" ) )
{
if ( mDisableInvalidGeometryHandling )
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 ) );
else
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 ) );
}
else
{
if ( mDisableInvalidGeometryHandling )
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 ) );
else
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 ) );
}
// we will first try to sample a small portion of the table/view, so the count of rows involved
// will be useful to evaluate if we have enough data to use the sample
statement += ", count(*)";
}
else
{
if ( mGeometryColType == QLatin1String( "geometry" ) )
{
if ( mDisableInvalidGeometryHandling )
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 ) );
else
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 ) );
}
else
{
statement = QStringLiteral( "select %1" ).arg( QgsMssqlUtils::quotedIdentifier( mGeometryColName ) );
readAllGeography = true;
}
}
if ( mIsQuery )
{
statement += QStringLiteral( " FROM (%1) q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
else
{
statement += QStringLiteral( " FROM %1.%2" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
if ( !mSqlWhereClause.isEmpty() )
{
statement += " where (" + mSqlWhereClause + ')';
}
}
if ( estimate )
{
// Try to use just 1% sample of the whole table/view to limit the amount of rows accessed.
// This heuristic may fail (e.g. when the table is small or when primary key values do not
// get sampled enough) so in case we do not have at least 10 features, we fall back to full
// traversal of the table/view
const int minSampleCount = 10;
QString cols, delim;
for ( const auto idx : mPrimaryKeyAttrs )
{
const QgsField &fld = mAttributeFields.at( idx );
cols += delim + QgsMssqlUtils::quotedIdentifier( fld.name() );
delim = QStringLiteral( "," );
}
// See https://docs.microsoft.com/en-us/previous-versions/software-testing/cc441928(v=msdn.10)
const QString sampleFilter = QString( "(ABS(CAST((BINARY_CHECKSUM(%1)) as int)) % 100) = 42" ).arg( cols );
const QString statementSample = statement + ( mSqlWhereClause.isEmpty() ? " WHERE " : " AND " ) + sampleFilter;
if ( LoggedExec( query, statementSample ) && query.next() && !QgsVariantUtils::isNull( query.value( 0 ) ) && query.value( 4 ).toInt() >= minSampleCount )
{
mExtent.setXMinimum( query.value( 0 ).toDouble() );
mExtent.setYMinimum( query.value( 1 ).toDouble() );
mExtent.setXMaximum( query.value( 2 ).toDouble() );
mExtent.setYMaximum( query.value( 3 ).toDouble() );
return;
}
}
if ( !LoggedExec( query, statement ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
}
if ( !query.isActive() )
{
return;
}
if ( !readAllGeography && query.next() )
{
if ( QgsVariantUtils::isNull( query.value( 0 ) )
|| QgsVariantUtils::isNull( query.value( 1 ) )
|| QgsVariantUtils::isNull( query.value( 2 ) )
|| QgsVariantUtils::isNull( query.value( 3 ) ) )
{
mExtent.setNull();
}
else
{
mExtent.setXMinimum( query.value( 0 ).toDouble() );
mExtent.setYMinimum( query.value( 1 ).toDouble() );
mExtent.setXMaximum( query.value( 2 ).toDouble() );
mExtent.setYMaximum( query.value( 3 ).toDouble() );
}
return;
}
// We have to read all the geometry if readAllGeography is true.
while ( query.next() )
{
QByteArray ar = query.value( 0 ).toByteArray();
std::unique_ptr<QgsAbstractGeometry> geom = mParser.parseSqlGeometry( reinterpret_cast<unsigned char *>( ar.data() ), ar.size() );
if ( geom )
{
const QgsRectangle rect = geom->boundingBox();
mExtent.combineExtentWith( rect );
mWkbType = geom->wkbType();
mSRId = mParser.GetSRSId();
}
}
}
// Return the extent of the layer
QgsRectangle QgsMssqlProvider::extent() const
{
if ( mExtent.isNull() )
UpdateStatistics( mUseEstimatedMetadata );
return mExtent;
}
/**
* Returns the feature type
*/
Qgis::WkbType QgsMssqlProvider::wkbType() const
{
return mWkbType;
}
/**
* Returns the feature type
*/
long long QgsMssqlProvider::featureCount() const
{
// Return the count that we get from the subset.
if ( !mSqlWhereClause.isEmpty() )
return mNumberFeatures;
// If there is no subset set we can get the count from the system tables.
// Which is faster then doing select count(*)
QSqlQuery query = createQuery();
query.setForwardOnly( true );
QString statement;
if ( !mIsQuery )
{
statement = QStringLiteral(
"SELECT rows"
" FROM sys.tables t"
" JOIN sys.partitions p ON t.object_id = p.object_id AND p.index_id IN (0,1)"
" WHERE SCHEMA_NAME(t.schema_id) = %1 AND OBJECT_NAME(t.OBJECT_ID) = %2"
)
.arg( QgsMssqlUtils::quotedValue( mSchemaName ), QgsMssqlUtils::quotedValue( mTableName ) );
}
else
{
statement = { QStringLiteral( R"raw(SELECT COUNT(*) FROM (%1) q)raw" )
.arg( mQuery ) };
}
if ( LoggedExec( query, statement ) && query.next() )
{
return query.value( 0 ).toLongLong();
}
else
{
// We couldn't get the rows from the sys tables. Can that ever happen?
// Should just do a select count(*) here.
QgsDebugError( QStringLiteral( "Could not retrieve feature count using %1: %2 " ).arg( statement, query.lastError().text() ) );
return static_cast< long long >( Qgis::FeatureCountState::UnknownCount );
}
}
QgsFields QgsMssqlProvider::fields() const
{
return mAttributeFields;
}
bool QgsMssqlProvider::isValid() const
{
return mValid;
}
Qgis::ProviderStyleStorageCapabilities QgsMssqlProvider::styleStorageCapabilities() const
{
Qgis::ProviderStyleStorageCapabilities storageCapabilities;
if ( isValid() )
{
storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase;
storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase;
}
return storageCapabilities;
}
bool QgsMssqlProvider::addFeatures( QgsFeatureList &flist, Flags flags )
{
if ( mIsQuery )
return false;
for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it )
{
if ( it->hasGeometry() && mWkbType == Qgis::WkbType::NoGeometry )
{
it->clearGeometry();
}
else if ( it->hasGeometry() && QgsWkbTypes::geometryType( it->geometry().wkbType() ) != QgsWkbTypes::geometryType( mWkbType ) )
{
pushError( tr( "Could not add feature with geometry type %1 to layer of type %2" ).arg( QgsWkbTypes::displayString( it->geometry().wkbType() ), QgsWkbTypes::displayString( mWkbType ) ) );
if ( !mSkipFailures )
return false;
continue;
}
QString statement;
QString values;
if ( !( flags & QgsFeatureSink::FastInsert ) )
{
statement += QLatin1String( "DECLARE @px TABLE (" );
QString delim;
for ( const auto idx : mPrimaryKeyAttrs )
{
const QgsField &fld = mAttributeFields.at( idx );
QString type = fld.typeName();
if ( type.endsWith( QLatin1String( " identity" ) ) )
type = type.left( type.length() - 9 );
if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) )
{
if ( fld.length() > 0 )
type = QStringLiteral( "%1(%2)" ).arg( type ).arg( fld.length() );
}
else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
{
if ( fld.length() > 0 && fld.precision() > 0 )
type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( fld.length() ).arg( fld.precision() );
}
statement += delim + QStringLiteral( "[%1] %2" ).arg( fld.name(), type );
delim = ",";
}
statement += "); ";
}
statement += QStringLiteral( "INSERT INTO [%1].[%2] (" ).arg( mSchemaName, mTableName );
bool first = true;
QSqlQuery query = createQuery();
query.setForwardOnly( true );
const QgsAttributes attrs = it->attributes();
for ( int i = 0; i < attrs.count(); ++i )
{
if ( i >= mAttributeFields.count() )
break;
const QgsField &fld = mAttributeFields.at( i );
if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
continue; // You can't update timestamp columns they are server only.
if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
continue; // skip identity field
if ( fld.name().isEmpty() )
continue; // invalid
if ( QgsVariantUtils::isUnsetAttributeValue( attrs.at( i ) ) )
continue;
if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
continue; // skip fields having default values
if ( mComputedColumns.contains( fld.name() ) )
continue; // skip computed columns because they are done server side.
if ( !first )
{
statement += ',';
values += ',';
}
else
first = false;
statement += QStringLiteral( "[%1]" ).arg( fld.name() );
values += QLatin1Char( '?' );
}
// append geometry column name
if ( !mGeometryColName.isEmpty() )
{
if ( !first )
{
statement += ',';
values += ',';
}
statement += QStringLiteral( "[%1]" ).arg( mGeometryColName );
if ( mGeometryColType == QLatin1String( "geometry" ) )
{
if ( mUseWkb )
values += QStringLiteral( "geometry::STGeomFromWKB(?,%1).MakeValid()" ).arg( mSRId );
else
values += QStringLiteral( "geometry::STGeomFromText(?,%1).MakeValid()" ).arg( mSRId );
}
else
{
if ( mUseWkb )
values += QStringLiteral( "geography::STGeomFromWKB(?,%1)" ).arg( mSRId );
else
values += QStringLiteral( "geography::STGeomFromText(?,%1)" ).arg( mSRId );
}
}
statement += QLatin1String( ") " );
if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
{
statement += QLatin1String( " OUTPUT " );
QString delim;
for ( const auto idx : std::as_const( mPrimaryKeyAttrs ) )
{
const QgsField &fld = mAttributeFields.at( idx );
statement += delim + QStringLiteral( "inserted.[%1]" ).arg( fld.name() );
delim = QStringLiteral( "," );
}
statement += QLatin1String( " INTO @px " );
}
statement += QStringLiteral( " VALUES (" ) + values + ')';
if ( !( flags & QgsFeatureSink::FastInsert && !mPrimaryKeyAttrs.isEmpty() ) )
{
statement += QLatin1String( "; SELECT * FROM @px;" );
}
// use prepared statement to prevent from sql injection
if ( !query.prepare( statement ) )
{
const QString msg = query.lastError().text();
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
if ( !mSkipFailures )
{
pushError( msg );
return false;
}
else
continue;
}
for ( int i = 0; i < attrs.count(); ++i )
{
if ( i >= mAttributeFields.count() )
break;
const QgsField &fld = mAttributeFields.at( i );
if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
continue; // You can't update timestamp columns they are server only.
if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
continue; // skip identity field
if ( fld.name().isEmpty() )
continue; // invalid
if ( QgsVariantUtils::isUnsetAttributeValue( attrs.at( i ) ) )
continue;
if ( mDefaultValues.contains( i ) && mDefaultValues.value( i ) == attrs.at( i ).toString() )
continue; // skip fields having default values
if ( mComputedColumns.contains( fld.name() ) )
continue; // skip computed columns because they are done server side.
const QMetaType::Type type = fld.type();
if ( QgsVariantUtils::isNull( attrs.at( i ) ) )
{
// binding null values
if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
else
query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
}
else if ( type == QMetaType::Type::Int )
{
// binding an INTEGER value
query.addBindValue( attrs.at( i ).toInt() );
}
else if ( type == QMetaType::Type::Double )
{
// binding a DOUBLE value
query.addBindValue( attrs.at( i ).toDouble() );
}
else if ( type == QMetaType::Type::QString )
{
// binding a TEXT value
query.addBindValue( attrs.at( i ).toString() );
}
else if ( type == QMetaType::Type::QTime )
{
// binding a TIME value
query.addBindValue( attrs.at( i ).toTime().toString( Qt::ISODate ) );
}
else if ( type == QMetaType::Type::QDate )
{
// binding a DATE value
query.addBindValue( attrs.at( i ).toDate().toString( Qt::ISODate ) );
}
else if ( type == QMetaType::Type::QDateTime )
{
// binding a DATETIME value
query.addBindValue( attrs.at( i ).toDateTime().toString( Qt::ISODate ) );
}
else
{
query.addBindValue( attrs.at( i ) );
}
}
if ( !mGeometryColName.isEmpty() )
{
QgsGeometry geom = it->geometry();
if ( QgsWkbTypes::isMultiType( mWkbType ) && !geom.isMultipart() )
{
geom.convertToMultiType();
}
if ( mUseWkb )
{
const QByteArray bytea = geom.asWkb();
query.addBindValue( bytea, QSql::In | QSql::Binary );
}
else
{
QString wkt;
if ( !geom.isNull() )
{
// Z and M on the end of a WKT string isn't valid for
// SQL Server so we have to remove it first.
wkt = geom.asWkt();
const thread_local QRegularExpression wktRx( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
wkt.replace( wktRx, QStringLiteral( "(" ) );
// if we have M value only, we need to insert null-s for the Z value
if ( QgsWkbTypes::hasM( geom.wkbType() ) && !QgsWkbTypes::hasZ( geom.wkbType() ) )
{
const thread_local QRegularExpression nullRx( QStringLiteral( "(?=\\s[0-9+-.]+[,)])" ) );
wkt.replace( QRegularExpression( nullRx ), QStringLiteral( " NULL" ) );
}
}
query.addBindValue( wkt );
}
}
if ( !LoggedExecPrepared( query ) )
{
const QString msg = query.lastError().text();
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
if ( !mSkipFailures )
{
pushError( msg );
return false;
}
}
if ( !( flags & QgsFeatureSink::FastInsert ) && !mPrimaryKeyAttrs.isEmpty() )
{
if ( !query.next() )
{
const QString msg = query.lastError().text();
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
if ( !mSkipFailures )
{
pushError( msg );
return false;
}
}
if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::Int )
{
it->setId( query.value( 0 ).toLongLong() );
}
else
{
QVariantList keyvals;
for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
{
keyvals << query.value( i );
}
it->setId( mShared->lookupFid( keyvals ) );
}
}
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
bool QgsMssqlProvider::addAttributes( const QList<QgsField> &attributes )
{
if ( attributes.isEmpty() )
return true;
if ( mIsQuery )
return false;
QString statement = QStringLiteral( "ALTER TABLE %1.%2 ADD " ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ) );
QStringList attributeClauses;
attributeClauses.reserve( attributes.size() );
for ( QList<QgsField>::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
{
QString type = it->typeName();
if ( type == QLatin1String( "char" ) || type == QLatin1String( "varchar" ) || type == QLatin1String( "nvarchar" ) )
{
if ( it->length() > 0 )
type = QStringLiteral( "%1(%2)" ).arg( type ).arg( it->length() );
}
else if ( type == QLatin1String( "numeric" ) || type == QLatin1String( "decimal" ) )
{
if ( it->length() > 0 && it->precision() > 0 )
type = QStringLiteral( "%1(%2,%3)" ).arg( type ).arg( it->length() ).arg( it->precision() );
}
attributeClauses.append( QStringLiteral( "[%1] %2" ).arg( it->name(), type ) );
}
statement += attributeClauses.join( QStringLiteral( ", " ) );
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, statement ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
return false;
}
loadFields();
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
bool QgsMssqlProvider::deleteAttributes( const QgsAttributeIds &attributes )
{
if ( mIsQuery )
return false;
QString statement;
for ( QgsAttributeIds::const_iterator it = attributes.begin(); it != attributes.end(); ++it )
{
if ( statement.isEmpty() )
{
statement = QStringLiteral( "ALTER TABLE [%1].[%2] DROP COLUMN " ).arg( mSchemaName, mTableName );
}
else
statement += ',';
statement += QStringLiteral( "[%1]" ).arg( mAttributeFields.at( *it ).name() );
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, statement ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
return false;
}
query.finish();
loadFields();
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
bool QgsMssqlProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map )
{
if ( mIsQuery )
return false;
if ( attr_map.isEmpty() )
return true;
if ( mPrimaryKeyAttrs.isEmpty() )
return false;
for ( QgsChangedAttributesMap::const_iterator it = attr_map.begin(); it != attr_map.end(); ++it )
{
const QgsFeatureId fid = it.key();
// skip added features
if ( FID_IS_NEW( fid ) )
continue;
const QgsAttributeMap &attrs = it.value();
if ( attrs.isEmpty() )
continue;
QString statement = QStringLiteral( "UPDATE [%1].[%2] SET " ).arg( mSchemaName, mTableName );
bool first = true;
bool pkChanged = false;
QSqlQuery query = createQuery();
query.setForwardOnly( true );
for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
{
const QgsField fld = mAttributeFields.at( it2.key() );
if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
continue; // You can't update timestamp columns they are server only.
if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
continue; // skip identity field
if ( fld.name().isEmpty() )
continue; // invalid
if ( QgsVariantUtils::isUnsetAttributeValue( it2.value() ) )
continue;
if ( mComputedColumns.contains( fld.name() ) )
continue; // skip computed columns because they are done server side.
if ( !first )
statement += ',';
else
first = false;
pkChanged = pkChanged || mPrimaryKeyAttrs.contains( it2.key() );
statement += QStringLiteral( "[%1]=?" ).arg( fld.name() );
}
if ( first )
return true; // no fields have been changed
// set attribute filter
statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
// use prepared statement to prevent from sql injection
if ( !query.prepare( statement ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
return false;
}
for ( QgsAttributeMap::const_iterator it2 = attrs.begin(); it2 != attrs.end(); ++it2 )
{
const QgsField fld = mAttributeFields.at( it2.key() );
if ( fld.typeName().compare( QLatin1String( "timestamp" ), Qt::CaseInsensitive ) == 0 )
continue; // You can't update timestamp columns they are server only.
if ( fld.typeName().endsWith( QLatin1String( " identity" ), Qt::CaseInsensitive ) )
continue; // skip identity field
if ( fld.name().isEmpty() )
continue; // invalid
if ( it2.value().userType() == qMetaTypeId< QgsUnsetAttributeValue >() )
continue;
if ( mComputedColumns.contains( fld.name() ) )
continue; // skip computed columns because they are done server side.
const QMetaType::Type type = fld.type();
if ( QgsVariantUtils::isNull( *it2 ) )
{
// binding null values
if ( type == QMetaType::Type::QDate || type == QMetaType::Type::QDateTime )
query.addBindValue( QgsVariantUtils::createNullVariant( QMetaType::Type::QString ) );
else
query.addBindValue( QgsVariantUtils::createNullVariant( type ) );
}
else if ( type == QMetaType::Type::Int )
{
// binding an INTEGER value
query.addBindValue( it2->toInt() );
}
else if ( type == QMetaType::Type::Double )
{
// binding a DOUBLE value
query.addBindValue( it2->toDouble() );
}
else if ( type == QMetaType::Type::QString )
{
// binding a TEXT value
query.addBindValue( it2->toString() );
}
else if ( type == QMetaType::Type::QDateTime )
{
// binding a DATETIME value
query.addBindValue( it2->toDateTime().toString( Qt::ISODate ) );
}
else if ( type == QMetaType::Type::QDate )
{
// binding a DATE value
query.addBindValue( it2->toDate().toString( Qt::ISODate ) );
}
else if ( type == QMetaType::Type::QTime )
{
// binding a TIME value
query.addBindValue( it2->toTime().toString( Qt::ISODate ) );
}
else
{
query.addBindValue( *it2 );
}
}
if ( !LoggedExecPrepared( query ) )
{
QgsDebugError( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) );
return false;
}
if ( pkChanged && mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::FidMap )
{
const QVariant v = mShared->removeFid( fid );
QVariantList k = v.toList();
for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
{
const int idx = mPrimaryKeyAttrs[i];
if ( !attrs.contains( idx ) )
continue;
k[i] = attrs[idx];
}
mShared->insertFid( fid, k );
}
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
bool QgsMssqlProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
{
if ( mIsQuery )
return false;
if ( geometry_map.isEmpty() )
return true;
if ( mPrimaryKeyAttrs.isEmpty() )
return false;
for ( QgsGeometryMap::const_iterator it = geometry_map.constBegin(); it != geometry_map.constEnd(); ++it )
{
const QgsFeatureId fid = it.key();
// skip added features
if ( FID_IS_NEW( fid ) )
continue;
QString statement;
statement = QStringLiteral( "UPDATE [%1].[%2] SET " ).arg( mSchemaName, mTableName );
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( mGeometryColType == QLatin1String( "geometry" ) )
{
if ( mUseWkb )
statement += QStringLiteral( "[%1]=geometry::STGeomFromWKB(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
else
statement += QStringLiteral( "[%1]=geometry::STGeomFromText(?,%2).MakeValid()" ).arg( mGeometryColName ).arg( mSRId );
}
else
{
if ( mUseWkb )
statement += QStringLiteral( "[%1]=geography::STGeomFromWKB(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
else
statement += QStringLiteral( "[%1]=geography::STGeomFromText(?,%2)" ).arg( mGeometryColName ).arg( mSRId );
}
// set attribute filter
statement += QStringLiteral( " WHERE " ) + whereClauseFid( fid );
if ( !query.prepare( statement ) )
{
pushError( query.lastError().text() );
return false;
}
// add geometry param
if ( mUseWkb )
{
const QByteArray bytea = it->asWkb();
query.addBindValue( bytea, QSql::In | QSql::Binary );
}
else
{
QString wkt = it->asWkt();
// Z and M on the end of a WKT string isn't valid for
// SQL Server so we have to remove it first.
const thread_local QRegularExpression zmRegExp( QStringLiteral( "[mzMZ]+\\s*\\(" ) );
wkt.replace( zmRegExp, QStringLiteral( "(" ) );
query.addBindValue( wkt );
}
if ( !LoggedExecPrepared( query ) )
{
pushError( query.lastError().text() );
return false;
}
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
bool QgsMssqlProvider::deleteFeatures( const QgsFeatureIds &ids )
{
if ( mIsQuery )
return false;
if ( mPrimaryKeyAttrs.isEmpty() )
return false;
if ( ids.empty() )
return true; // for consistency providers return true to an empty list
if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::Int )
{
QString featureIds, delim;
for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
{
featureIds += delim + FID_TO_STRING( *it );
delim = QStringLiteral( "," );
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
const QString statement = QStringLiteral( "DELETE FROM [%1].[%2] WHERE [%3] IN (%4)" ).arg( mSchemaName, mTableName, mAttributeFields.at( mPrimaryKeyAttrs[0] ).name(), featureIds );
if ( LoggedExec( query, statement ) )
{
if ( query.numRowsAffected() == ids.size() )
{
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
pushError( tr( "Only %1 of %2 features deleted" ).arg( query.numRowsAffected() ).arg( ids.size() ) );
}
else
{
pushError( query.lastError().text() );
}
}
else if ( mPrimaryKeyType == QgsMssqlDatabase::PrimaryKeyType::FidMap )
{
int i = 0;
QSqlQuery query = createQuery();
for ( QgsFeatureIds::const_iterator it = ids.begin(); it != ids.end(); ++it )
{
const QString statement = QStringLiteral( "DELETE FROM [%1].[%2] WHERE %3" ).arg( mSchemaName, mTableName, whereClauseFid( *it ) );
if ( LoggedExec( query, statement ) )
{
if ( query.numRowsAffected() == 1 )
{
mShared->removeFid( *it );
i++;
}
}
else
{
pushError( query.lastError().text() );
break;
}
}
if ( i == ids.size() )
{
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return true;
}
if ( i > 0 )
pushError( tr( "Only %1 of %2 features deleted" ).arg( i ).arg( ids.size() ) );
}
return false;
}
void QgsMssqlProvider::updateExtents()
{
mExtent.setNull();
}
Qgis::VectorProviderCapabilities QgsMssqlProvider::capabilities() const
{
Qgis::VectorProviderCapabilities cap;
const bool hasGeom = !mGeometryColName.isEmpty();
if ( !mIsQuery )
{
cap |= Qgis::VectorProviderCapability::CreateAttributeIndex | Qgis::VectorProviderCapability::AddFeatures | Qgis::VectorProviderCapability::AddAttributes | Qgis::VectorProviderCapability::TransactionSupport;
if ( hasGeom )
{
cap |= Qgis::VectorProviderCapability::CreateSpatialIndex;
}
}
if ( mPrimaryKeyAttrs.isEmpty() )
return cap;
cap |= Qgis::VectorProviderCapability::SelectAtId;
if ( hasGeom && !mIsQuery )
cap |= Qgis::VectorProviderCapability::ChangeGeometries;
if ( !mIsQuery )
cap |= Qgis::VectorProviderCapability::DeleteFeatures | Qgis::VectorProviderCapability::ChangeAttributeValues | Qgis::VectorProviderCapability::DeleteAttributes;
return cap;
}
bool QgsMssqlProvider::createSpatialIndex()
{
if ( mUseEstimatedMetadata )
UpdateStatistics( false );
QSqlQuery query = createQuery();
query.setForwardOnly( true );
QString statement;
statement = QStringLiteral( "CREATE SPATIAL INDEX [qgs_%1_sidx] ON [%2].[%3] ( [%4] )" ).arg( mGeometryColName, mSchemaName, mTableName, mGeometryColName );
if ( mGeometryColType == QLatin1String( "geometry" ) )
{
if ( mExtent.isNull() )
return false;
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() ) );
}
else
{
statement += QLatin1String( " USING GEOGRAPHY_GRID" );
}
if ( !LoggedExec( query, statement ) )
{
pushError( query.lastError().text() );
return false;
}
return true;
}
bool QgsMssqlProvider::createAttributeIndex( int field )
{
QSqlQuery query = createQuery();
query.setForwardOnly( true );
QString statement;
if ( field < 0 || field >= mAttributeFields.size() )
{
pushError( QStringLiteral( "createAttributeIndex invalid index" ) );
return false;
}
statement = QStringLiteral( "CREATE NONCLUSTERED INDEX [qgs_%1_idx] ON [%2].[%3] ( [%4] )" ).arg( mGeometryColName, mSchemaName, mTableName, mAttributeFields.at( field ).name() );
if ( !LoggedExec( query, statement ) )
{
pushError( query.lastError().text() );
return false;
}
return true;
}
QgsCoordinateReferenceSystem QgsMssqlProvider::crs() const
{
if ( !mCrs.isValid() && mSRId > 0 )
{
// try to load crs from the database tables as a fallback
QSqlQuery query = createQuery();
query.setForwardOnly( true );
const QString statement { QStringLiteral( "SELECT srtext FROM spatial_ref_sys WHERE srid=%1" ).arg( mSRId ) };
bool execOk = LoggedExec( query, statement );
if ( execOk && query.isActive() )
{
if ( query.next() )
{
mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
if ( mCrs.isValid() )
return mCrs;
}
query.finish();
}
query.clear();
query.setForwardOnly( true );
// Look in the system reference table for the data if we can't find it yet
execOk = LoggedExec( query, QStringLiteral( "SELECT well_known_text FROM sys.spatial_reference_systems WHERE spatial_reference_id=%1" ).arg( mSRId ) );
if ( execOk && query.isActive() && query.next() )
{
mCrs = QgsCoordinateReferenceSystem::fromWkt( query.value( 0 ).toString() );
if ( mCrs.isValid() )
return mCrs;
}
else // try to load as EPSG
{
mCrs = QgsCoordinateReferenceSystem::fromEpsgId( mSRId );
}
}
return mCrs;
}
void QgsMssqlProvider::setTransaction( QgsTransaction *transaction )
{
// static_cast since layers cannot be added to a transaction of a non-matching provider
mTransaction = static_cast<QgsMssqlTransaction *>( transaction );
}
QgsTransaction *QgsMssqlProvider::transaction() const
{
return mTransaction;
}
std::shared_ptr<QgsMssqlDatabase> QgsMssqlProvider::connection() const
{
return mTransaction ? mTransaction->conn() : QgsMssqlDatabase::connectDb( uri().connectionInfo(), false );
}
void QgsMssqlProvider::handlePostCloneOperations( QgsVectorDataProvider *source )
{
mShared = qobject_cast<QgsMssqlProvider *>( source )->mShared;
}
QString QgsMssqlProvider::subsetString() const
{
return mSqlWhereClause;
}
QString QgsMssqlProvider::name() const
{
return MSSQL_PROVIDER_KEY;
}
bool QgsMssqlProvider::setSubsetString( const QString &theSQL, bool )
{
if ( theSQL.trimmed() == mSqlWhereClause )
return true;
const QString prevWhere = mSqlWhereClause;
mSqlWhereClause = theSQL.trimmed();
QString sql;
if ( mIsQuery )
{
sql = QStringLiteral( "SELECT count(*) FROM %1 q %2" ).arg( mQuery, !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
else
{
sql = QStringLiteral( "SELECT count(*) FROM %1.%2 %3" ).arg( QgsMssqlUtils::quotedIdentifier( mSchemaName ), QgsMssqlUtils::quotedIdentifier( mTableName ), !mSqlWhereClause.isEmpty() ? QStringLiteral( " WHERE (%1)" ).arg( mSqlWhereClause ) : QString() );
}
QSqlQuery query = createQuery();
query.setForwardOnly( true );
if ( !LoggedExec( query, sql ) )
{
pushError( query.lastError().text() );
mSqlWhereClause = prevWhere;
return false;
}
if ( query.isActive() && query.next() )
mNumberFeatures = query.value( 0 ).toLongLong();
QgsDataSourceUri anUri = QgsDataSourceUri( dataSourceUri() );
anUri.setSql( mSqlWhereClause );
setDataSourceUri( anUri.uri() );
mExtent.setNull();
emit dataChanged();
return true;
}
bool QgsMssqlProvider::supportsSubsetString() const
{
return true;
}
QString QgsMssqlProvider::subsetStringDialect() const
{
return tr( "SQL Server WHERE clause" );
}
QString QgsMssqlProvider::subsetStringHelpUrl() const
{
return QStringLiteral( "https://learn.microsoft.com/en-us/sql/t-sql/queries/where-transact-sql?view=sql-server-ver16" );
}
QString QgsMssqlProvider::description() const
{
return MSSQL_PROVIDER_DESCRIPTION;
}
QgsAttributeList QgsMssqlProvider::pkAttributeIndexes() const
{
return mPrimaryKeyAttrs;
}
QString QgsMssqlProvider::geometryColumnName() const
{
QGIS_PROTECT_QOBJECT_THREAD_ACCESS
return mGeometryColName;
}
QStringList QgsMssqlProvider::subLayers() const
{
return mTables;
}
Qgis::VectorLayerTypeFlags QgsMssqlProvider::vectorLayerTypeFlags() const
{
Qgis::VectorLayerTypeFlags flags;
if ( mValid && mIsQuery )
{
flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery );
}
return flags;
}
bool QgsMssqlProvider::convertField( QgsField &field )
{
QString fieldType = QStringLiteral( "nvarchar(max)" ); //default to string
int fieldSize = field.length();
int fieldPrec = field.precision();
switch ( field.type() )
{
case QMetaType::Type::LongLong:
fieldType = QStringLiteral( "bigint" );
fieldSize = -1;
fieldPrec = 0;
break;
case QMetaType::Type::QDateTime:
fieldType = QStringLiteral( "datetime" );
fieldPrec = 0;
break;
case QMetaType::Type::QDate:
fieldType = QStringLiteral( "date" );
fieldPrec = 0;
break;
case QMetaType::Type::QTime:
fieldType = QStringLiteral( "time" );
fieldPrec = 0;
break;
case QMetaType::Type::QString:
fieldType = QStringLiteral( "nvarchar(max)" );
fieldPrec = 0;
break;
case QMetaType::Type::Int:
fieldType = QStringLiteral( "int" );
fieldSize = -1;
fieldPrec = 0;
break;
case QMetaType::Type::Double:
if ( fieldSize <= 0 || fieldPrec <= 0 )
{
fieldType = QStringLiteral( "float" );
fieldSize = -1;
fieldPrec = 0;
}
else
{
fieldType = QStringLiteral( "decimal" );
}
break;
default:
return false;
}
field.setTypeName( fieldType );
field.setLength( fieldSize );
field.setPrecision( fieldPrec );
return true;
}
void QgsMssqlProvider::mssqlWkbTypeAndDimension( Qgis::WkbType wkbType, QString &geometryType, int &dim )
{
const Qgis::WkbType flatType = QgsWkbTypes::flatType( wkbType );
if ( flatType == Qgis::WkbType::Point )
geometryType = QStringLiteral( "POINT" );
else if ( flatType == Qgis::WkbType::LineString )
geometryType = QStringLiteral( "LINESTRING" );
else if ( flatType == Qgis::WkbType::Polygon )
geometryType = QStringLiteral( "POLYGON" );
else if ( flatType == Qgis::WkbType::MultiPoint )
geometryType = QStringLiteral( "MULTIPOINT" );
else if ( flatType == Qgis::WkbType::MultiLineString )
geometryType = QStringLiteral( "MULTILINESTRING" );
else if ( flatType == Qgis::WkbType::MultiPolygon )
geometryType = QStringLiteral( "MULTIPOLYGON" );
else if ( flatType == Qgis::WkbType::GeometryCollection )
geometryType = QStringLiteral( "GEOMETRYCOLLECTION" );
else if ( flatType == Qgis::WkbType::CircularString )
geometryType = QStringLiteral( "CIRCULARSTRING" );
else if ( flatType == Qgis::WkbType::CompoundCurve )
geometryType = QStringLiteral( "COMPOUNDCURVE" );
else if ( flatType == Qgis::WkbType::CurvePolygon )
geometryType = QStringLiteral( "CURVEPOLYGON" );
else if ( flatType == Qgis::WkbType::Unknown )
geometryType = QStringLiteral( "GEOMETRY" );
else
{
dim = 0;
return;
}
if ( QgsWkbTypes::hasZ( wkbType ) && QgsWkbTypes::hasM( wkbType ) )
{
dim = 4;
}
else if ( QgsWkbTypes::hasZ( wkbType ) )
{
dim = 3;
}
else if ( QgsWkbTypes::hasM( wkbType ) )
{
geometryType += QLatin1Char( 'M' );
dim = 3;
}
else if ( wkbType >= Qgis::WkbType::Point25D && wkbType <= Qgis::WkbType::MultiPolygon25D )
{
dim = 3;
}
}
Qgis::WkbType QgsMssqlProvider::getWkbType( const QString &geometryType )
{
return QgsWkbTypes::parseType( geometryType );
}
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 )
{
// populate members from the uri structure
QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
if ( errorMessage )
*errorMessage = db->errorText();
return Qgis::VectorExportResult::ErrorConnectionFailed;
}
createdLayerUri = uri;
const QString dbName = dsUri.database();
QString schemaName = dsUri.schema();
const QString tableName = dsUri.table();
QString geometryColumn = dsUri.geometryColumn();
QString primaryKey = dsUri.keyColumn();
if ( schemaName.isEmpty() )
schemaName = QStringLiteral( "dbo" );
if ( wkbType != Qgis::WkbType::NoGeometry && geometryColumn.isEmpty() )
geometryColumn = QStringLiteral( "geom" );
// if no pk name was passed, define the new pk field name
if ( primaryKey.isEmpty() )
{
int index = 0;
const QString pk = primaryKey = QStringLiteral( "qgs_fid" );
for ( int i = 0, n = fields.size(); i < n; ++i )
{
if ( fields.at( i ).name() == primaryKey )
{
// it already exists, try again with a new name
primaryKey = QStringLiteral( "%1_%2" ).arg( pk ).arg( index++ );
i = 0;
}
}
}
QString sql;
QSqlQuery q = QSqlQuery( db->db() );
q.setForwardOnly( true );
// initialize metadata tables (same as OGR SQL)
sql = QString( "IF OBJECT_ID(N'[geometry_columns]', N'U') IS NULL "
"CREATE TABLE geometry_columns (f_table_catalog varchar(128) not null, "
"f_table_schema varchar(128) not null, f_table_name varchar(256) not null, "
"f_geometry_column varchar(256) not null, coord_dimension integer not null, "
"srid integer not null, geometry_type varchar(30) not null, "
"CONSTRAINT geometry_columns_pk PRIMARY KEY (f_table_catalog, "
"f_table_schema, f_table_name, f_geometry_column));\n"
"IF OBJECT_ID(N'[spatial_ref_sys]', N'U') IS NULL "
"CREATE TABLE spatial_ref_sys (srid integer not null "
"PRIMARY KEY, auth_name varchar(256), auth_srid integer, srtext varchar(2048), proj4text varchar(2048))" );
auto logWrapper = std::make_unique<QgsDatabaseQueryLogWrapper>( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN );
if ( !q.exec( sql ) )
{
logWrapper->setError( q.lastError().text() );
if ( errorMessage )
*errorMessage = q.lastError().text();
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
// set up spatial reference id
long srid = 0;
if ( srs.isValid() )
{
srid = srs.postgisSrid();
QString auth_srid = QStringLiteral( "null" );
QString auth_name = QStringLiteral( "null" );
QStringList sl = srs.authid().split( ':' );
if ( sl.length() == 2 )
{
auth_name = sl[0];
auth_srid = sl[1];
}
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)" )
.arg( srid )
.arg( QgsMssqlUtils::quotedValue( auth_name ), auth_srid, QgsMssqlUtils::quotedValue( srs.toWkt() ), QgsMssqlUtils::quotedValue( srs.toProj() ) );
logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
if ( !q.exec( sql ) )
{
logWrapper->setError( q.lastError().text() );
if ( errorMessage )
*errorMessage = q.lastError().text();
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
}
// get wkb type and dimension
QString geometryType;
int dim = 2;
mssqlWkbTypeAndDimension( wkbType, geometryType, dim );
if ( overwrite )
{
// remove the old table with the same name
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;" )
.arg( schemaName, tableName );
logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
if ( !q.exec( sql ) )
{
logWrapper->setError( q.lastError().text() );
if ( errorMessage )
*errorMessage = q.lastError().text();
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
}
else
{
// test for existing
sql = QStringLiteral( "SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')" )
.arg( schemaName, tableName );
logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
if ( !q.exec( sql ) )
{
logWrapper->setError( q.lastError().text() );
if ( errorMessage )
*errorMessage = q.lastError().text();
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
// if we got a hit, abort!!
if ( q.next() )
{
if ( errorMessage )
*errorMessage = tr( "Table [%1].[%2] already exists" ).arg( schemaName, tableName );
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
}
if ( !geometryColumn.isEmpty() )
{
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"
"CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL, [%4] [geometry] NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
"DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
"INSERT INTO [geometry_columns] ([f_table_catalog], [f_table_schema],[f_table_name], "
"[f_geometry_column],[coord_dimension],[srid],[geometry_type]) VALUES ('%5', '%1', '%2', '%4', %6, %7, '%8')" )
.arg( schemaName, tableName, primaryKey, geometryColumn, dbName, QString::number( dim ), QString::number( srid ), geometryType );
}
else
{
//geometryless table
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"
"CREATE TABLE [%1].[%2]([%3] [int] IDENTITY(1,1) NOT NULL CONSTRAINT [PK_%2] PRIMARY KEY CLUSTERED ( [%3] ASC ))\n"
"DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'\n"
)
.arg( schemaName, tableName, primaryKey );
}
logWrapper.reset( new QgsDatabaseQueryLogWrapper( sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProvider" ), QGS_QUERY_LOG_ORIGIN ) );
if ( !q.exec( sql ) )
{
logWrapper->setError( q.lastError().text() );
if ( errorMessage )
*errorMessage = q.lastError().text();
return Qgis::VectorExportResult::ErrorCreatingLayer;
}
// clear any resources hold by the query
q.clear();
q.setForwardOnly( true );
// use the provider to edit the table
dsUri.setDataSource( schemaName, tableName, geometryColumn, QString(), primaryKey );
const QgsDataProvider::ProviderOptions providerOptions;
const Qgis::DataProviderReadFlags flags;
auto provider = std::make_unique< QgsMssqlProvider >( dsUri.uri(), providerOptions, flags );
if ( !provider->isValid() )
{
if ( errorMessage )
*errorMessage = QObject::tr( "Loading of the MSSQL provider failed" );
return Qgis::VectorExportResult::ErrorInvalidLayer;
}
// add fields to the layer
if ( oldToNewAttrIdxMap )
oldToNewAttrIdxMap->clear();
if ( fields.size() > 0 )
{
const QgsFields providerFields = provider->fields();
int offset = providerFields.size();
// get the list of fields
QList<QgsField> flist;
for ( int originalFieldIndex = 0, n = fields.size(); originalFieldIndex < n; ++originalFieldIndex )
{
QgsField field = fields.at( originalFieldIndex );
if ( field.name() == geometryColumn )
{
// Found a field with the same name of the geometry column. Skip it!
continue;
}
const int providerIndex = providerFields.lookupField( field.name() );
if ( providerIndex >= 0 )
{
// we've already created this field (i.e. it was set in the CREATE TABLE statement), so
// we don't need to re-add it now
if ( oldToNewAttrIdxMap )
oldToNewAttrIdxMap->insert( originalFieldIndex, providerIndex );
continue;
}
if ( !( options && options->value( QStringLiteral( "skipConvertFields" ), false ).toBool() ) && !convertField( field ) )
{
if ( errorMessage )
*errorMessage = QObject::tr( "Unsupported type for field %1" ).arg( field.name() );
return Qgis::VectorExportResult::ErrorAttributeTypeUnsupported;
}
flist.append( field );
if ( oldToNewAttrIdxMap )
oldToNewAttrIdxMap->insert( originalFieldIndex, offset++ );
}
if ( !provider->addAttributes( flist ) )
{
if ( errorMessage )
*errorMessage = QObject::tr( "Creation of fields failed" );
return Qgis::VectorExportResult::ErrorAttributeCreationFailed;
}
}
return Qgis::VectorExportResult::Success;
}
/**
* Class factory to return a pointer to a newly created
* QgsMssqlProvider object
*/
QgsMssqlProvider *QgsMssqlProviderMetadata::createProvider(
const QString &uri,
const QgsDataProvider::ProviderOptions &options,
Qgis::DataProviderReadFlags flags
)
{
return new QgsMssqlProvider( uri, options, flags );
}
QList<QgsDataItemProvider *> QgsMssqlProviderMetadata::dataItemProviders() const
{
QList<QgsDataItemProvider *> providers;
providers << new QgsMssqlDataItemProvider;
return providers;
}
QgsTransaction *QgsMssqlProviderMetadata::createTransaction( const QString &connString )
{
return new QgsMssqlTransaction( connString );
}
QMap<QString, QgsAbstractProviderConnection *> QgsMssqlProviderMetadata::connections( bool cached )
{
return connectionsProtected<QgsMssqlProviderConnection, QgsMssqlConnection>( cached );
}
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &name )
{
return new QgsMssqlProviderConnection( name );
}
QgsAbstractProviderConnection *QgsMssqlProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration )
{
return new QgsMssqlProviderConnection( uri, configuration );
}
void QgsMssqlProviderMetadata::deleteConnection( const QString &name )
{
deleteConnectionProtected<QgsMssqlProviderConnection>( name );
}
void QgsMssqlProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name )
{
saveConnectionProtected( conn, name );
}
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 )
{
return QgsMssqlProvider::createEmptyLayer(
uri, fields, wkbType, srs, overwrite,
&oldToNewAttrIdxMap, createdLayerUri, &errorMessage, options
);
}
QString buildfTableCatalogClause( const QgsDataSourceUri &dsUri )
{
return QStringLiteral( "f_table_catalog%1" ).arg( dsUri.database().isEmpty() ? QStringLiteral( " IS NULL" ) : QStringLiteral( "=%1" ).arg( QgsMssqlUtils::quotedValue( dsUri.database() ) ) );
}
bool QgsMssqlProviderMetadata::styleExists( const QString &uri, const QString &styleId, QString &errorCause )
{
errorCause.clear();
const QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
errorCause = QObject::tr( "Error connecting to database: %1" ).arg( db->errorText() );
return false;
}
QSqlQuery query = QSqlQuery( db->db() );
query.setForwardOnly( true );
const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME = N'layer_styles'" ) };
if ( !LoggedExecMetadata( query, sql, uri ) )
{
errorCause = QObject::tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
return false;
}
if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
{
// no layer_styles table
query.finish();
return false;
}
query.clear();
query.setForwardOnly( true );
const QString checkQuery = QString( "SELECT styleName"
" FROM layer_styles"
" WHERE %1"
" AND f_table_schema=%2"
" AND f_table_name=%3"
" AND f_geometry_column=%4"
" AND styleName=%5" )
.arg( buildfTableCatalogClause( dsUri ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
.arg( QgsMssqlUtils::quotedValue( styleId.isEmpty() ? dsUri.table() : styleId ) );
if ( !LoggedExecMetadata( query, checkQuery, uri ) )
{
errorCause = QObject::tr( "Checking for style failed: %1" ).arg( query.lastError().text() );
return false;
}
if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleId )
{
return true;
}
else
{
return false;
}
}
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 )
{
const QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
QgsDebugError( QStringLiteral( "Error connecting to database" ) );
QgsDebugError( db->errorText() );
return false;
}
QSqlQuery query = QSqlQuery( db->db() );
query.setForwardOnly( true );
QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
if ( !LoggedExecMetadata( query, sql, uri ) )
{
QgsDebugError( query.lastError().text() );
return false;
}
if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
{
QgsDebugMsgLevel( QStringLiteral( "Need to create styles table" ), 2 );
sql = QStringLiteral( "CREATE TABLE [dbo].[layer_styles]("
"[id] int IDENTITY(1,1) PRIMARY KEY,"
"[f_table_catalog] [varchar](1024) NULL,"
"[f_table_schema] [varchar](1024) NULL,"
"[f_table_name] [varchar](1024) NULL,"
"[f_geometry_column] [varchar](1024) NULL,"
"[styleName] [varchar](1024) NULL,"
"[styleQML] [text] NULL,"
"[styleSLD] [text] NULL,"
"[useAsDefault] [int] NULL,"
"[description] [text] NULL,"
"[owner] [varchar](1024) NULL,"
"[ui] [text] NULL,"
"[update_time] [datetime] NULL"
") ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]" );
const bool execOk = LoggedExecMetadata( query, sql, uri );
if ( !execOk )
{
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" ) };
errCause = error;
return false;
}
query.finish();
query.clear();
query.setForwardOnly( true );
}
QString uiFileColumn;
QString uiFileValue;
if ( !uiFileContent.isEmpty() )
{
uiFileColumn = QStringLiteral( ",ui" );
uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( uiFileContent );
}
QgsDebugMsgLevel( QStringLiteral( "Ready to insert new style" ), 2 );
// Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values
// can contain user entered strings, which may themselves include %## values that would be
// replaced by the QString.arg function. To ensure that the final SQL string is not corrupt these
// two values are both replaced in the final .arg call of the string construction.
sql = QStringLiteral( "INSERT INTO layer_styles"
"(f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner%11"
") VALUES ("
"%1,%2,%3,%4,%5,%6,%7,%8,%9,%10%12"
")" )
.arg( QgsMssqlUtils::quotedValue( dsUri.database() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
.arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) )
.arg( QgsMssqlUtils::quotedValue( qmlStyle ) )
.arg( QgsMssqlUtils::quotedValue( sldStyle ) )
.arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
.arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.username() ) )
.arg( uiFileColumn )
.arg( uiFileValue );
const QString checkQuery = QStringLiteral( "SELECT styleName"
" FROM layer_styles"
" WHERE %1"
" AND f_table_schema=%2"
" AND f_table_name=%3"
" AND f_geometry_column=%4"
" AND styleName=%5" )
.arg( buildfTableCatalogClause( dsUri ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
.arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
if ( !LoggedExecMetadata( query, checkQuery, uri ) )
{
QgsDebugError( query.lastError().text() );
QgsDebugError( QStringLiteral( "Check Query failed" ) );
return false;
}
if ( query.isActive() && query.next() && query.value( 0 ).toString() == styleName )
{
QgsDebugMsgLevel( QStringLiteral( "Updating styles" ), 2 );
sql = QString( "UPDATE layer_styles "
" SET useAsDefault=%1"
",styleQML=%2"
",styleSLD=%3"
",description=%4"
",owner=%5"
" WHERE %6"
" AND f_table_schema=%7"
" AND f_table_name=%8"
" AND f_geometry_column=%9"
" AND styleName=%10" )
.arg( useAsDefault ? QStringLiteral( "1" ) : QStringLiteral( "0" ) )
.arg( QgsMssqlUtils::quotedValue( qmlStyle ) )
.arg( QgsMssqlUtils::quotedValue( sldStyle ) )
.arg( QgsMssqlUtils::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.username() ) )
.arg( buildfTableCatalogClause( dsUri ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) )
.arg( QgsMssqlUtils::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) );
}
if ( useAsDefault )
{
const QString removeDefaultSql = QString( "UPDATE layer_styles "
" SET useAsDefault=0"
" WHERE %1"
" AND f_table_schema=%2"
" AND f_table_name=%3"
" AND f_geometry_column=%4" )
.arg( buildfTableCatalogClause( dsUri ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
sql = QStringLiteral( "%1; %2;" ).arg( removeDefaultSql, sql );
}
QgsDebugMsgLevel( QStringLiteral( "Inserting styles" ), 2 );
QgsDebugMsgLevel( sql, 2 );
const bool execOk = LoggedExecMetadata( query, sql, uri );
if ( !execOk )
{
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." );
}
return execOk;
}
QString QgsMssqlProviderMetadata::loadStyle( const QString &uri, QString &errCause )
{
QString styleName;
return loadStoredStyle( uri, styleName, errCause );
}
QString QgsMssqlProviderMetadata::loadStoredStyle( const QString &uri, QString &styleName, QString &errCause )
{
errCause.clear();
const QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
QgsDebugError( QStringLiteral( "Error connecting to database" ) );
QgsDebugError( db->errorText() );
errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
return QString();
}
QSqlQuery query = QSqlQuery( db->db() );
query.setForwardOnly( true );
const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
if ( !LoggedExecMetadata( query, sql, uri ) )
{
errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
return QString();
}
if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
{
// no layer_styles table
errCause = tr( "Style does not exist" );
query.finish();
return QString();
}
query.clear();
query.setForwardOnly( true );
const QString selectQmlQuery = QString( "SELECT top 1 styleName, styleQML"
" FROM layer_styles"
" WHERE %1"
" AND f_table_schema=%2"
" AND f_table_name=%3"
" AND f_geometry_column=%4"
" ORDER BY useAsDefault desc" )
.arg( buildfTableCatalogClause( dsUri ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
if ( !LoggedExecMetadata( query, selectQmlQuery, uri ) )
{
QgsDebugMsgLevel( QStringLiteral( "Load of style failed" ), 2 );
const QString msg = query.lastError().text();
errCause = msg;
QgsDebugError( msg );
return QString();
}
if ( query.isActive() && query.next() )
{
styleName = query.value( 0 ).toString();
const QString style = query.value( 1 ).toString();
return style;
}
return QString();
}
int QgsMssqlProviderMetadata::listStyles( const QString &uri, QStringList &ids, QStringList &names, QStringList &descriptions, QString &errCause )
{
const QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
QgsDebugError( QStringLiteral( "Error connecting to database" ) );
QgsDebugError( db->errorText() );
return -1;
}
QSqlQuery query = QSqlQuery( db->db() );
query.setForwardOnly( true );
QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
// check if layer_styles table already exist
if ( !LoggedExecMetadata( query, sql, uri ) )
{
const QString msg = query.lastError().text();
errCause = msg;
QgsDebugError( msg );
return -1;
}
if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
{
QgsDebugMsgLevel( QStringLiteral( "No styles available on DB" ), 2 );
return -1;
}
const QString fTableCatalogClause = buildfTableCatalogClause( dsUri );
const QString selectRelatedQuery = QString( "SELECT id,styleName,description"
" FROM layer_styles "
" WHERE %1"
" AND f_table_schema=%2"
" AND f_table_name=%3"
" AND f_geometry_column=%4"
" ORDER BY useasdefault DESC, update_time DESC" )
.arg( fTableCatalogClause )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
bool queryOk = LoggedExecMetadata( query, selectRelatedQuery, uri );
if ( !queryOk )
{
QgsDebugError( query.lastError().text() );
return -1;
}
int numberOfRelatedStyles = 0;
while ( query.isActive() && query.next() )
{
QgsDebugMsgLevel( query.value( 1 ).toString(), 2 );
ids.append( query.value( 0 ).toString() );
names.append( query.value( 1 ).toString() );
descriptions.append( query.value( 2 ).toString() );
numberOfRelatedStyles = numberOfRelatedStyles + 1;
}
const QString selectOthersQuery = QString( "SELECT id,styleName,description"
" FROM layer_styles "
" WHERE NOT (%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)"
" ORDER BY update_time DESC" )
.arg( fTableCatalogClause )
.arg( QgsMssqlUtils::quotedValue( dsUri.schema() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.table() ) )
.arg( QgsMssqlUtils::quotedValue( dsUri.geometryColumn() ) );
QgsDebugMsgLevel( selectOthersQuery, 2 );
queryOk = LoggedExecMetadata( query, selectOthersQuery, uri );
if ( !queryOk )
{
QgsDebugError( query.lastError().text() );
return -1;
}
while ( query.next() )
{
ids.append( query.value( 0 ).toString() );
names.append( query.value( 1 ).toString() );
descriptions.append( query.value( 2 ).toString() );
}
return numberOfRelatedStyles;
}
QgsMssqlProviderMetadata::QgsMssqlProviderMetadata()
: QgsProviderMetadata( QgsMssqlProvider::MSSQL_PROVIDER_KEY, QgsMssqlProvider::MSSQL_PROVIDER_DESCRIPTION )
{
}
QIcon QgsMssqlProviderMetadata::icon() const
{
return QgsApplication::getThemeIcon( QStringLiteral( "mIconMssql.svg" ) );
}
QString QgsMssqlProviderMetadata::getStyleById( const QString &uri, const QString &styleId, QString &errCause )
{
const QgsDataSourceUri dsUri( uri );
// connect to database
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri );
if ( !db->isValid() )
{
QgsDebugError( QStringLiteral( "Error connecting to database" ) );
QgsDebugError( db->errorText() );
errCause = tr( "Cannot connect to database: %1" ).arg( db->errorText() );
return QString();
}
QSqlQuery query = QSqlQuery( db->db() );
query.setForwardOnly( true );
const QString sql { QStringLiteral( "SELECT COUNT(*) FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_NAME= N'layer_styles'" ) };
if ( !LoggedExecMetadata( query, sql, uri ) )
{
errCause = tr( "Could not check if layer_styles table exists: %1" ).arg( query.lastError().text() );
return QString();
}
if ( query.isActive() && query.next() && query.value( 0 ).toInt() == 0 )
{
// no layer_styles table
errCause = tr( "Style does not exist" );
query.finish();
return QString();
}
query.clear();
query.setForwardOnly( true );
QString style;
const QString selectQmlQuery = QStringLiteral( "SELECT styleQml FROM layer_styles WHERE id=%1" ).arg( QgsMssqlUtils::quotedValue( styleId ) );
const bool queryOk = LoggedExecMetadata( query, selectQmlQuery, uri );
if ( !queryOk )
{
QgsDebugError( query.lastError().text() );
errCause = query.lastError().text();
return QString();
}
if ( !query.next() )
{
errCause = tr( "Style does not exist" );
}
else
{
style = query.value( 0 ).toString();
}
return style;
}
QVariantMap QgsMssqlProviderMetadata::decodeUri( const QString &uri ) const
{
const QgsDataSourceUri dsUri { uri };
QVariantMap uriParts;
if ( !dsUri.database().isEmpty() )
uriParts[QStringLiteral( "dbname" )] = dsUri.database();
if ( !dsUri.host().isEmpty() )
uriParts[QStringLiteral( "host" )] = dsUri.host();
if ( !dsUri.port().isEmpty() )
uriParts[QStringLiteral( "port" )] = dsUri.port();
if ( !dsUri.service().isEmpty() )
uriParts[QStringLiteral( "service" )] = dsUri.service();
if ( !dsUri.username().isEmpty() )
uriParts[QStringLiteral( "username" )] = dsUri.username();
if ( !dsUri.password().isEmpty() )
uriParts[QStringLiteral( "password" )] = dsUri.password();
// Supported?
//if ( ! dsUri.authConfigId().isEmpty() )
// uriParts[ QStringLiteral( "authcfg" ) ] = dsUri.authConfigId();
if ( dsUri.wkbType() != Qgis::WkbType::Unknown )
uriParts[QStringLiteral( "type" )] = static_cast<quint32>( dsUri.wkbType() );
// Supported?
// uriParts[ QStringLiteral( "selectatid" ) ] = dsUri.selectAtIdDisabled();
if ( !dsUri.table().isEmpty() )
uriParts[QStringLiteral( "table" )] = dsUri.table();
if ( !dsUri.schema().isEmpty() )
uriParts[QStringLiteral( "schema" )] = dsUri.schema();
if ( !dsUri.keyColumn().isEmpty() )
uriParts[QStringLiteral( "key" )] = dsUri.keyColumn();
if ( !dsUri.srid().isEmpty() )
uriParts[QStringLiteral( "srid" )] = dsUri.srid();
uriParts[QStringLiteral( "estimatedmetadata" )] = dsUri.useEstimatedMetadata();
// is this supported?
// uriParts[ QStringLiteral( "sslmode" ) ] = dsUri.sslMode();
if ( !dsUri.sql().isEmpty() )
uriParts[QStringLiteral( "sql" )] = dsUri.sql();
if ( !dsUri.geometryColumn().isEmpty() )
uriParts[QStringLiteral( "geometrycolumn" )] = dsUri.geometryColumn();
// From configuration
static const QStringList configurationParameters {
QStringLiteral( "geometryColumnsOnly" ),
QStringLiteral( "allowGeometrylessTables" ),
QStringLiteral( "saveUsername" ),
QStringLiteral( "savePassword" ),
QStringLiteral( "estimatedMetadata" ),
QStringLiteral( "disableInvalidGeometryHandling" ),
QStringLiteral( "extentInGeometryColumns" ),
QStringLiteral( "primaryKeyInGeometryColumns" )
};
for ( const auto &configParam : configurationParameters )
{
if ( dsUri.hasParam( configParam ) )
{
uriParts[configParam] = dsUri.param( configParam );
}
}
return uriParts;
}
QString QgsMssqlProviderMetadata::encodeUri( const QVariantMap &parts ) const
{
QgsDataSourceUri dsUri;
if ( parts.contains( QStringLiteral( "dbname" ) ) )
dsUri.setDatabase( parts.value( QStringLiteral( "dbname" ) ).toString() );
// Also accepts "database"
if ( parts.contains( QStringLiteral( "database" ) ) )
dsUri.setDatabase( parts.value( QStringLiteral( "database" ) ).toString() );
// Supported?
//if ( parts.contains( QStringLiteral( "port" ) ) )
// dsUri.setParam( QStringLiteral( "port" ), parts.value( QStringLiteral( "port" ) ).toString() );
if ( parts.contains( QStringLiteral( "host" ) ) )
dsUri.setParam( QStringLiteral( "host" ), parts.value( QStringLiteral( "host" ) ).toString() );
if ( parts.contains( QStringLiteral( "service" ) ) )
dsUri.setParam( QStringLiteral( "service" ), parts.value( QStringLiteral( "service" ) ).toString() );
if ( parts.contains( QStringLiteral( "username" ) ) )
dsUri.setUsername( parts.value( QStringLiteral( "username" ) ).toString() );
if ( parts.contains( QStringLiteral( "password" ) ) )
dsUri.setPassword( parts.value( QStringLiteral( "password" ) ).toString() );
// Supported?
//if ( parts.contains( QStringLiteral( "authcfg" ) ) )
// dsUri.setAuthConfigId( parts.value( QStringLiteral( "authcfg" ) ).toString() );
if ( parts.contains( QStringLiteral( "type" ) ) )
dsUri.setParam( QStringLiteral( "type" ), QgsWkbTypes::displayString( static_cast<Qgis::WkbType>( parts.value( QStringLiteral( "type" ) ).toInt() ) ) );
// Supported?
//if ( parts.contains( QStringLiteral( "selectatid" ) ) )
// dsUri.setParam( QStringLiteral( "selectatid" ), parts.value( QStringLiteral( "selectatid" ) ).toString() );
if ( parts.contains( QStringLiteral( "table" ) ) )
dsUri.setTable( parts.value( QStringLiteral( "table" ) ).toString() );
if ( parts.contains( QStringLiteral( "schema" ) ) )
dsUri.setSchema( parts.value( QStringLiteral( "schema" ) ).toString() );
if ( parts.contains( QStringLiteral( "key" ) ) )
dsUri.setParam( QStringLiteral( "key" ), parts.value( QStringLiteral( "key" ) ).toString() );
if ( parts.contains( QStringLiteral( "srid" ) ) )
dsUri.setSrid( parts.value( QStringLiteral( "srid" ) ).toString() );
if ( parts.contains( QStringLiteral( "estimatedmetadata" ) ) )
dsUri.setParam( QStringLiteral( "estimatedmetadata" ), parts.value( QStringLiteral( "estimatedmetadata" ) ).toString() );
// Supported?
//if ( parts.contains( QStringLiteral( "sslmode" ) ) )
// dsUri.setParam( QStringLiteral( "sslmode" ), QgsDataSourceUri::encodeSslMode( static_cast<QgsDataSourceUri::SslMode>( parts.value( QStringLiteral( "sslmode" ) ).toInt( ) ) ) );
if ( parts.contains( QStringLiteral( "sql" ) ) )
dsUri.setSql( parts.value( QStringLiteral( "sql" ) ).toString() );
// Supported?
//if ( parts.contains( QStringLiteral( "checkPrimaryKeyUnicity" ) ) )
// dsUri.setParam( QStringLiteral( "checkPrimaryKeyUnicity" ), parts.value( QStringLiteral( "checkPrimaryKeyUnicity" ) ).toString() );
if ( parts.contains( QStringLiteral( "geometrycolumn" ) ) )
dsUri.setGeometryColumn( parts.value( QStringLiteral( "geometrycolumn" ) ).toString() );
if ( parts.contains( QStringLiteral( "disableInvalidGeometryHandling" ) ) )
dsUri.setParam( QStringLiteral( "disableInvalidGeometryHandling" ), parts.value( QStringLiteral( "disableInvalidGeometryHandling" ) ).toString() );
if ( parts.contains( QStringLiteral( "allowGeometrylessTables" ) ) )
dsUri.setParam( QStringLiteral( "allowGeometrylessTables" ), parts.value( QStringLiteral( "allowGeometrylessTables" ) ).toString() );
if ( parts.contains( QStringLiteral( "geometryColumnsOnly" ) ) )
dsUri.setParam( QStringLiteral( "geometryColumnsOnly" ), parts.value( QStringLiteral( "geometryColumnsOnly" ) ).toString() );
if ( parts.contains( QStringLiteral( "extentInGeometryColumns" ) ) )
dsUri.setParam( QStringLiteral( "extentInGeometryColumns" ), parts.value( QStringLiteral( "extentInGeometryColumns" ) ).toString() );
if ( parts.contains( QStringLiteral( "primaryKeyInGeometryColumns" ) ) )
dsUri.setParam( QStringLiteral( "primaryKeyInGeometryColumns" ), parts.value( QStringLiteral( "primaryKeyInGeometryColumns" ) ).toString() );
return dsUri.uri();
}
QList<Qgis::LayerType> QgsMssqlProviderMetadata::supportedLayerTypes() const
{
return { Qgis::LayerType::Vector };
}
QString QgsMssqlProvider::typeFromMetadata( const QString &typeName, int numCoords )
{
QString type { typeName };
const bool hasM { typeName.endsWith( 'M', Qt::CaseInsensitive ) };
if ( numCoords == 4 )
{
if ( hasM )
{
type.chop( 1 );
}
type.append( QStringLiteral( "ZM" ) );
}
else if ( numCoords == 3 )
{
if ( !hasM )
{
type.append( QStringLiteral( "Z" ) );
}
}
return type;
}
bool QgsMssqlProviderMetadata::execLogged( QSqlQuery &qry, const QString &sql, const QString &uri, const QString &queryOrigin ) const
{
QgsDatabaseQueryLogWrapper logWrapper { sql, uri, QStringLiteral( "mssql" ), QStringLiteral( "QgsMssqlProviderMetadata" ), queryOrigin };
const bool res { qry.exec( sql ) };
if ( !res )
{
logWrapper.setError( qry.lastError().text() );
}
else
{
if ( qry.isSelect() )
{
logWrapper.setFetchedRows( qry.size() );
}
else
{
logWrapper.setFetchedRows( qry.numRowsAffected() );
}
}
logWrapper.setQuery( qry.lastQuery() );
return res;
}
QGISEXTERN QgsProviderMetadata *providerMetadataFactory()
{
return new QgsMssqlProviderMetadata();
}
// ----------
QgsFeatureId QgsMssqlSharedData::lookupFid( const QVariantList &v )
{
const QMutexLocker locker( &mMutex );
const QMap<QVariantList, QgsFeatureId>::const_iterator it = mKeyToFid.constFind( v );
if ( it != mKeyToFid.constEnd() )
{
return it.value();
}
mFidToKey.insert( ++mFidCounter, v );
mKeyToFid.insert( v, mFidCounter );
return mFidCounter;
}
QVariant QgsMssqlSharedData::removeFid( QgsFeatureId fid )
{
const QMutexLocker locker( &mMutex );
const QVariantList v = mFidToKey[fid];
mFidToKey.remove( fid );
mKeyToFid.remove( v );
return v;
}
void QgsMssqlSharedData::insertFid( QgsFeatureId fid, const QVariantList &k )
{
const QMutexLocker locker( &mMutex );
mFidToKey.insert( fid, k );
mKeyToFid.insert( k, fid );
}
QVariantList QgsMssqlSharedData::lookupKey( QgsFeatureId featureId )
{
const QMutexLocker locker( &mMutex );
const QMap<QgsFeatureId, QVariantList>::const_iterator it = mFidToKey.find( featureId );
if ( it != mFidToKey.constEnd() )
return it.value();
return QVariantList();
}
QString QgsMssqlProvider::whereClauseFid( QgsFeatureId featureId )
{
QString whereClause;
switch ( mPrimaryKeyType )
{
case QgsMssqlDatabase::PrimaryKeyType::Int:
Q_ASSERT( mPrimaryKeyAttrs.size() == 1 );
whereClause = QStringLiteral( "[%1]=%2" ).arg( mAttributeFields.at( mPrimaryKeyAttrs[0] ).name(), FID_TO_STRING( featureId ) );
break;
case QgsMssqlDatabase::PrimaryKeyType::FidMap:
{
const QVariantList &pkVals = mShared->lookupKey( featureId );
if ( !pkVals.isEmpty() )
{
Q_ASSERT( pkVals.size() == mPrimaryKeyAttrs.size() );
whereClause = QStringLiteral( "(" );
QString delim;
for ( int i = 0; i < mPrimaryKeyAttrs.size(); ++i )
{
const QgsField &fld = mAttributeFields.at( mPrimaryKeyAttrs[i] );
whereClause += QStringLiteral( "%1[%2]=%3" ).arg( delim, fld.name(), QgsMssqlUtils::quotedValue( pkVals[i] ) );
delim = QStringLiteral( " AND " );
}
whereClause += QLatin1Char( ')' );
}
else
{
QgsDebugError( QStringLiteral( "FAILURE: Key values for feature %1 not found." ).arg( featureId ) );
whereClause = QStringLiteral( "NULL IS NOT NULL" );
}
}
break;
case QgsMssqlDatabase::PrimaryKeyType::Unknown:
Q_ASSERT( !"FAILURE: Primary key unknown" );
whereClause = QStringLiteral( "NULL IS NOT NULL" );
break;
}
return whereClause;
}
/* static */
QStringList QgsMssqlProvider::parseUriKey( const QString &key )
{
if ( key.isEmpty() )
return QStringList();
QStringList cols;
// remove quotes from key list
if ( key.startsWith( '"' ) && key.endsWith( '"' ) )
{
int i = 1;
QString col;
while ( i < key.size() )
{
if ( key[i] == '"' )
{
if ( i + 1 < key.size() && key[i + 1] == '"' )
{
i++;
}
else
{
cols << col;
col.clear();
if ( ++i == key.size() )
break;
Q_ASSERT( key[i] == ',' );
i++;
Q_ASSERT( key[i] == '"' );
i++;
col.clear();
continue;
}
}
col += key[i++];
}
}
else if ( key.contains( ',' ) )
{
cols = key.split( ',' );
}
else
{
cols << key;
}
return cols;
}
bool QgsMssqlProvider::getExtentFromGeometryColumns( QgsRectangle &extent ) const
{
QSqlQuery query = createQuery();
query.setForwardOnly( true );
const QString sql = QStringLiteral( "SELECT qgis_xmin,qgis_xmax,qgis_ymin,qgis_ymax "
"FROM geometry_columns WHERE f_table_name = %1 AND f_table_schema = %2 "
"AND NOT (qgis_xmin IS NULL OR qgis_xmax IS NULL OR qgis_ymin IS NULL OR qgis_ymax IS NULL)" );
const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) );
if ( LoggedExec( query, statement ) && query.isActive() )
{
query.next();
if ( query.isValid() )
{
extent.setXMinimum( query.value( 0 ).toDouble() );
extent.setXMaximum( query.value( 1 ).toDouble() );
extent.setYMinimum( query.value( 2 ).toDouble() );
extent.setYMaximum( query.value( 3 ).toDouble() );
return true;
}
}
return false;
}
bool QgsMssqlProvider::getPrimaryKeyFromGeometryColumns( QStringList &primaryKeys )
{
QSqlQuery query = createQuery();
query.setForwardOnly( true );
primaryKeys.clear();
const QString sql = QStringLiteral( "SELECT qgis_pkey FROM geometry_columns "
"WHERE f_table_name = %1 AND f_table_schema = %2 AND NOT qgis_pkey IS NULL" );
const QString statement = sql.arg( QgsMssqlUtils::quotedValue( mTableName ), QgsMssqlUtils::quotedValue( mSchemaName ) );
if ( LoggedExec( query, statement ) && query.isActive() )
{
query.next();
if ( query.isValid() )
{
primaryKeys = query.value( 0 ).toString().split( ',' );
if ( !primaryKeys.isEmpty() )
return true;
}
}
return false;
}