mirror of
https://github.com/qgis/QGIS.git
synced 2025-10-03 00:04:47 -04:00
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.
3173 lines
104 KiB
C++
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;
|
|
}
|