Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[feature] Transactions in MSSQL provider
  • Loading branch information
wonder-sk committed Sep 9, 2021
1 parent be3a9e7 commit 1a8ff84
Show file tree
Hide file tree
Showing 21 changed files with 956 additions and 281 deletions.
4 changes: 2 additions & 2 deletions python/core/auto_generated/qgstransaction.sip.in
Expand Up @@ -110,15 +110,15 @@ returns the last created savepoint if it's not dirty
.. versionadded:: 3.0
%End

QString createSavepoint( const QString &savePointId, QString &error /Out/ );
virtual QString createSavepoint( const QString &savePointId, QString &error /Out/ );
%Docstring
creates a save point
returns empty string on error

.. versionadded:: 3.0
%End

bool rollbackToSavepoint( const QString &name, QString &error /Out/ );
virtual bool rollbackToSavepoint( const QString &name, QString &error /Out/ );
%Docstring
rollback to save point, the save point is maintained and is "undertied"

Expand Down
11 changes: 1 addition & 10 deletions src/core/qgstransaction.cpp
Expand Up @@ -223,16 +223,7 @@ QString QgsTransaction::createSavepoint( QString &error SIP_OUT )
}

const QString name( QStringLiteral( "qgis" ) + ( QUuid::createUuid().toString().mid( 1, 24 ).replace( '-', QString() ) ) );

if ( !executeSql( QStringLiteral( "SAVEPOINT %1" ).arg( QgsExpression::quotedColumnRef( name ) ), error ) )
{
QgsMessageLog::logMessage( tr( "Could not create savepoint (%1)" ).arg( error ) );
return QString();
}

mSavepoints.push( name );
mLastSavePointIsDirty = false;
return name;
return createSavepoint( name, error );
}

QString QgsTransaction::createSavepoint( const QString &savePointId, QString &error SIP_OUT )
Expand Down
11 changes: 5 additions & 6 deletions src/core/qgstransaction.h
Expand Up @@ -132,13 +132,13 @@ class CORE_EXPORT QgsTransaction : public QObject SIP_ABSTRACT
* returns empty string on error
* \since QGIS 3.0
*/
QString createSavepoint( const QString &savePointId, QString &error SIP_OUT );
virtual QString createSavepoint( const QString &savePointId, QString &error SIP_OUT );

/**
* rollback to save point, the save point is maintained and is "undertied"
* \since QGIS 3.0
*/
bool rollbackToSavepoint( const QString &name, QString &error SIP_OUT );
virtual bool rollbackToSavepoint( const QString &name, QString &error SIP_OUT );

/**
* dirty save point such that next call to createSavepoint will create a new one
Expand Down Expand Up @@ -179,18 +179,17 @@ class CORE_EXPORT QgsTransaction : public QObject SIP_ABSTRACT
QgsTransaction( const QString &connString ) SIP_SKIP;

QString mConnString;
bool mTransactionActive;
QStack< QString > mSavepoints;
bool mLastSavePointIsDirty;

private slots:
void onLayerDeleted();

private:

bool mTransactionActive;
QSet<QgsVectorLayer *> mLayers;

QStack< QString > mSavepoints;
bool mLastSavePointIsDirty;

void setLayerTransactionIds( QgsTransaction *transaction );

static QString removeLayerIdOrName( const QString &str );
Expand Down
26 changes: 26 additions & 0 deletions src/providers/mssql/CMakeLists.txt
Expand Up @@ -8,6 +8,8 @@ set(MSSQL_SRCS
qgsmssqlexpressioncompiler.cpp
qgsmssqlfeatureiterator.cpp
qgsmssqlgeomcolumntypethread.cpp
qgsmssqltransaction.cpp
qgsmssqldatabase.cpp
)

if (WITH_GUI)
Expand Down Expand Up @@ -49,6 +51,30 @@ if (WITH_GUI)
add_dependencies(provider_mssql ui)
endif()

# static library
add_library (provider_mssql_a STATIC ${MSSQL_SRCS} ${MSSQL_HDRS})

# require c++17
target_compile_features(provider_mssql_a PRIVATE cxx_std_17)

target_link_libraries (provider_mssql_a
qgis_core
${POSTGRES_LIBRARY}
${Qt5Xml_LIBRARIES}
${Qt5Core_LIBRARIES}
${Qt5Svg_LIBRARIES}
${Qt5Network_LIBRARIES}
${Qt5Sql_LIBRARIES}
${Qt5Concurrent_LIBRARIES}
)

if (WITH_GUI)
target_link_libraries (provider_mssql_a
qgis_gui
)
add_dependencies(provider_mssql_a ui)
endif()

########################################################
# Install

Expand Down
179 changes: 20 additions & 159 deletions src/providers/mssql/qgsmssqlconnection.cpp
Expand Up @@ -17,147 +17,16 @@

#include "qgsmssqlconnection.h"
#include "qgsmssqlprovider.h"
#include "qgsmssqldatabase.h"
#include "qgslogger.h"
#include "qgssettings.h"
#include "qgsdatasourceuri.h"
#include <QSqlDatabase>
#include <QThread>
#include <QSqlError>
#include <QSqlQuery>
#include <QSet>
#include <QCoreApplication>
#include <QFile>

int QgsMssqlConnection::sConnectionId = 0;
#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
QMutex QgsMssqlConnection::sMutex { QMutex::Recursive };
#else
QRecursiveMutex QgsMssqlConnection::sMutex;
#endif

QSqlDatabase QgsMssqlConnection::getDatabase( const QString &service, const QString &host, const QString &database, const QString &username, const QString &password )
{
QSqlDatabase db;
QString connectionName;

// create a separate database connection for each feature source
if ( service.isEmpty() )
{
if ( !host.isEmpty() )
connectionName = host + '.';

if ( database.isEmpty() )
{
QgsDebugMsg( QStringLiteral( "QgsMssqlProvider database name not specified" ) );
return db;
}

connectionName += QStringLiteral( "%1.%2" ).arg( database ).arg( sConnectionId++ );
}
else
connectionName = service;

// while everything we use from QSqlDatabase here is thread safe, we need to ensure
// that the connection cleanup on thread finalization happens in a predictable order
QMutexLocker locker( &sMutex );

const QString threadSafeConnectionName = dbConnectionName( connectionName );

if ( !QSqlDatabase::contains( threadSafeConnectionName ) )
{
db = QSqlDatabase::addDatabase( QStringLiteral( "QODBC" ), threadSafeConnectionName );
db.setConnectOptions( QStringLiteral( "SQL_ATTR_CONNECTION_POOLING=SQL_CP_ONE_PER_HENV" ) );

// for background threads, remove database when current thread finishes
if ( QThread::currentThread() != QCoreApplication::instance()->thread() )
{
QgsDebugMsgLevel( QStringLiteral( "Scheduled auth db remove on thread close" ), 2 );

// IMPORTANT - we use a direct connection here, because the database removal must happen immediately
// when the thread finishes, and we cannot let this get queued on the main thread's event loop.
// Otherwise, the QSqlDatabase's private data's thread gets reset immediately the QThread::finished,
// and a subsequent call to QSqlDatabase::database with the same thread address (yep it happens, actually a lot)
// triggers a condition in QSqlDatabase which detects the nullptr private thread data and returns an invalid database instead.
// QSqlDatabase::removeDatabase is thread safe, so this is ok to do.
QObject::connect( QThread::currentThread(), &QThread::finished, QThread::currentThread(), [threadSafeConnectionName]
{
const QMutexLocker locker( &sMutex );
QSqlDatabase::removeDatabase( threadSafeConnectionName );
}, Qt::DirectConnection );
}
}
else
{
db = QSqlDatabase::database( threadSafeConnectionName );
}
locker.unlock();

db.setHostName( host );
QString connectionString;
if ( !service.isEmpty() )
{
// driver was specified explicitly
connectionString = service;
}
else
{
#ifdef Q_OS_WIN
connectionString = "driver={SQL Server}";
#elif defined (Q_OS_MAC)
QString freeTDSDriver( QCoreApplication::applicationDirPath().append( "/lib/libtdsodbc.so" ) );
if ( QFile::exists( freeTDSDriver ) )
{
connectionString = QStringLiteral( "driver=%1;port=1433;TDS_Version=auto" ).arg( freeTDSDriver );
}
else
{
connectionString = QStringLiteral( "driver={FreeTDS};port=1433;TDS_Version=auto" );
}
#else
// It seems that FreeTDS driver by default uses an ancient TDS protocol version (4.2) to communicate with MS SQL
// which was causing various data corruption errors, for example:
// - truncating data from varchar columns to 255 chars - failing to read WKT for CRS
// - truncating binary data to 4096 bytes (see @@TEXTSIZE) - failing to parse larger geometries
// The added "TDS_Version=auto" should negotiate more recent version (manually setting e.g. 7.2 worked fine too)
connectionString = QStringLiteral( "driver={FreeTDS};port=1433;TDS_Version=auto" );
#endif
}

if ( !host.isEmpty() )
connectionString += ";server=" + host;

if ( !database.isEmpty() )
connectionString += ";database=" + database;

if ( password.isEmpty() )
connectionString += QLatin1String( ";trusted_connection=yes" );
else
connectionString += ";uid=" + username + ";pwd=" + password;

if ( !username.isEmpty() )
db.setUserName( username );

if ( !password.isEmpty() )
db.setPassword( password );

db.setDatabaseName( connectionString );

// only uncomment temporarily -- it can show connection password otherwise!
// QgsDebugMsg( connectionString );
return db;
}

bool QgsMssqlConnection::openDatabase( QSqlDatabase &db )
{
if ( !db.isOpen() )
{
if ( !db.open() )
{
return false;
}
}
return true;
}

bool QgsMssqlConnection::geometryColumnsOnly( const QString &name )
{
Expand Down Expand Up @@ -236,18 +105,18 @@ bool QgsMssqlConnection::dropView( const QString &uri, QString *errorMessage )
const QgsDataSourceUri dsUri( uri );

// connect to database
QSqlDatabase db = getDatabase( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
const QString schema = dsUri.schema();
const QString table = dsUri.table();

if ( !openDatabase( db ) )
if ( !db->isValid() )
{
if ( errorMessage )
*errorMessage = db.lastError().text();
*errorMessage = db->errorText();
return false;
}

QSqlQuery q = QSqlQuery( db );
QSqlQuery q = QSqlQuery( db->db() );
if ( !q.exec( QString( "DROP VIEW [%1].[%2]" ).arg( schema, table ) ) )
{
if ( errorMessage )
Expand All @@ -263,18 +132,18 @@ bool QgsMssqlConnection::dropTable( const QString &uri, QString *errorMessage )
const QgsDataSourceUri dsUri( uri );

// connect to database
QSqlDatabase db = getDatabase( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
const QString schema = dsUri.schema();
const QString table = dsUri.table();

if ( !openDatabase( db ) )
if ( !db->isValid() )
{
if ( errorMessage )
*errorMessage = db.lastError().text();
*errorMessage = db->errorText();
return false;
}

QSqlQuery q = QSqlQuery( db );
QSqlQuery q = QSqlQuery( db->db() );
q.setForwardOnly( true );
const QString sql = QString( "IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[%1].[%2]') AND type in (N'U')) DROP TABLE [%1].[%2]\n"
"DELETE FROM geometry_columns WHERE f_table_schema = '%1' AND f_table_name = '%2'" )
Expand All @@ -295,18 +164,18 @@ bool QgsMssqlConnection::truncateTable( const QString &uri, QString *errorMessag
const QgsDataSourceUri dsUri( uri );

// connect to database
QSqlDatabase db = getDatabase( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
std::shared_ptr<QgsMssqlDatabase> db = QgsMssqlDatabase::connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password() );
const QString schema = dsUri.schema();
const QString table = dsUri.table();

if ( !openDatabase( db ) )
if ( !db->isValid() )
{
if ( errorMessage )
*errorMessage = db.lastError().text();
*errorMessage = db->errorText();
return false;
}

QSqlQuery q = QSqlQuery( db );
QSqlQuery q = QSqlQuery( db->db() );
q.setForwardOnly( true );
const QString sql = QStringLiteral( "TRUNCATE TABLE [%1].[%2]" ).arg( schema, table );
if ( !q.exec( sql ) )
Expand All @@ -324,16 +193,16 @@ bool QgsMssqlConnection::createSchema( const QString &uri, const QString &schema
const QgsDataSourceUri dsUri( uri );

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

if ( !openDatabase( db ) )
if ( !db->isValid() )
{
if ( errorMessage )
*errorMessage = db.lastError().text();
*errorMessage = db->errorText();
return false;
}

QSqlQuery q = QSqlQuery( db );
QSqlQuery q = QSqlQuery( db->db() );
q.setForwardOnly( true );
const QString sql = QStringLiteral( "CREATE SCHEMA [%1]" ).arg( schemaName );
if ( !q.exec( sql ) )
Expand All @@ -351,14 +220,14 @@ QStringList QgsMssqlConnection::schemas( const QString &uri, QString *errorMessa
const QgsDataSourceUri dsUri( uri );

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

return schemas( db, errorMessage );
return schemas( db->db(), errorMessage );
}

QStringList QgsMssqlConnection::schemas( QSqlDatabase &dataBase, QString *errorMessage )
{
if ( !openDatabase( dataBase ) )
if ( !QgsMssqlDatabase::openDatabase( dataBase ) )
{
if ( errorMessage )
*errorMessage = dataBase.lastError().text();
Expand Down Expand Up @@ -599,11 +468,3 @@ QString QgsMssqlConnection::buildQueryForTables( const QString &connName )
{
return buildQueryForTables( allowGeometrylessTables( connName ), geometryColumnsOnly( connName ), excludedSchemasList( connName ) );
}

QString QgsMssqlConnection::dbConnectionName( const QString &name )
{
// Starting with Qt 5.11, sharing the same connection between threads is not allowed.
// We use a dedicated connection for each thread requiring access to the database,
// using the thread address as connection name.
return QStringLiteral( "%1:0x%2" ).arg( name ).arg( reinterpret_cast<quintptr>( QThread::currentThread() ), 2 * QT_POINTER_SIZE, 16, QLatin1Char( '0' ) );
}

0 comments on commit 1a8ff84

Please sign in to comment.