1 /***************************************************************************
2 qgsmssqldatabase.cpp
3 --------------------------------------
4 Date : August 2021
5 Copyright : (C) 2021 by Martin Dobias
6 Email : wonder dot sk at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
16 #include "qgsmssqldatabase.h"
17
18 #include "qgsdatasourceuri.h"
19 #include "qgslogger.h"
20
21 #include <QCoreApplication>
22 #include <QtDebug>
23 #include <QFile>
24 #include <QThread>
25
26
27 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
28 QMutex QgsMssqlDatabase::sMutex { QMutex::Recursive };
29 #else
30 QRecursiveMutex QgsMssqlDatabase::sMutex;
31 #endif
32
33 QMap<QString, std::weak_ptr<QgsMssqlDatabase> > QgsMssqlDatabase::sConnections;
34
35
connectionName(const QString & service,const QString & host,const QString & database,bool transaction)36 QString QgsMssqlDatabase::connectionName( const QString &service, const QString &host, const QString &database, bool transaction )
37 {
38 QString connName;
39 if ( service.isEmpty() )
40 {
41 if ( !host.isEmpty() )
42 connName = host + '.';
43
44 if ( database.isEmpty() )
45 {
46 QgsDebugMsg( QStringLiteral( "QgsMssqlProvider database name not specified" ) );
47 return QString();
48 }
49
50 connName += database;
51 }
52 else
53 connName = service;
54
55 if ( !transaction )
56 connName += QStringLiteral( ":0x%1" ).arg( reinterpret_cast<quintptr>( QThread::currentThread() ), 2 * QT_POINTER_SIZE, 16, QLatin1Char( '0' ) );
57 else
58 connName += ":transaction";
59 return connName;
60 }
61
62
connectDb(const QString & uri,bool transaction)63 std::shared_ptr<QgsMssqlDatabase> QgsMssqlDatabase::connectDb( const QString &uri, bool transaction )
64 {
65 QgsDataSourceUri dsUri( uri );
66 return connectDb( dsUri.service(), dsUri.host(), dsUri.database(), dsUri.username(), dsUri.password(), transaction );
67 }
68
connectDb(const QString & service,const QString & host,const QString & database,const QString & username,const QString & password,bool transaction)69 std::shared_ptr<QgsMssqlDatabase> QgsMssqlDatabase::connectDb( const QString &service, const QString &host, const QString &database, const QString &username, const QString &password, bool transaction )
70 {
71 // try to use existing conn or create a new one
72
73 QMutexLocker locker( &sMutex );
74
75 QString connName = connectionName( service, host, database, transaction );
76
77 if ( sConnections.contains( connName ) && !sConnections[connName].expired() )
78 return sConnections[connName].lock();
79
80 QSqlDatabase db = getDatabase( service, host, database, username, password, transaction );
81
82 std::shared_ptr<QgsMssqlDatabase> c( new QgsMssqlDatabase( db, transaction ) );
83
84 // we return connection even if it failed to open (because the error message may be useful)
85 // but do not add it to connections as it is not useful
86 if ( !c->isValid() )
87 return c;
88
89 sConnections[connName] = c;
90 return c;
91 }
92
QgsMssqlDatabase(const QSqlDatabase & db,bool transaction)93 QgsMssqlDatabase::QgsMssqlDatabase( const QSqlDatabase &db, bool transaction )
94 {
95 mTransaction = transaction;
96 mDB = db;
97
98 if ( mTransaction )
99 {
100 #if QT_VERSION < QT_VERSION_CHECK(5, 14, 0)
101 mTransactionMutex.reset( new QMutex { QMutex::Recursive } );
102 #else
103 mTransactionMutex.reset( new QRecursiveMutex );
104 #endif
105 }
106
107 if ( !mDB.isOpen() )
108 {
109 if ( !mDB.open() )
110 {
111 QgsDebugMsg( "Failed to open MSSQL database: " + mDB.lastError().text() );
112 }
113 }
114 }
115
~QgsMssqlDatabase()116 QgsMssqlDatabase::~QgsMssqlDatabase()
117 {
118 // close DB if it is open
119 if ( mDB.isOpen() )
120 {
121 mDB.close();
122 }
123 }
124
125
126 // -------------------
127
128
getDatabase(const QString & service,const QString & host,const QString & database,const QString & username,const QString & password,bool transaction)129 QSqlDatabase QgsMssqlDatabase::getDatabase( const QString &service, const QString &host, const QString &database, const QString &username, const QString &password, bool transaction )
130 {
131 QSqlDatabase db;
132
133 // while everything we use from QSqlDatabase here is thread safe, we need to ensure
134 // that the connection cleanup on thread finalization happens in a predictable order
135 QMutexLocker locker( &sMutex );
136
137 const QString threadSafeConnectionName = connectionName( service, host, database, transaction );
138
139 if ( !QSqlDatabase::contains( threadSafeConnectionName ) )
140 {
141 db = QSqlDatabase::addDatabase( QStringLiteral( "QODBC" ), threadSafeConnectionName );
142 db.setConnectOptions( QStringLiteral( "SQL_ATTR_CONNECTION_POOLING=SQL_CP_ONE_PER_HENV" ) );
143
144 // for background threads, remove database when current thread finishes
145 if ( QThread::currentThread() != QCoreApplication::instance()->thread() )
146 {
147 QgsDebugMsgLevel( QStringLiteral( "Scheduled auth db remove on thread close" ), 2 );
148
149 // IMPORTANT - we use a direct connection here, because the database removal must happen immediately
150 // when the thread finishes, and we cannot let this get queued on the main thread's event loop.
151 // Otherwise, the QSqlDatabase's private data's thread gets reset immediately the QThread::finished,
152 // and a subsequent call to QSqlDatabase::database with the same thread address (yep it happens, actually a lot)
153 // triggers a condition in QSqlDatabase which detects the nullptr private thread data and returns an invalid database instead.
154 // QSqlDatabase::removeDatabase is thread safe, so this is ok to do.
155 QObject::connect( QThread::currentThread(), &QThread::finished, QThread::currentThread(), [threadSafeConnectionName]
156 {
157 const QMutexLocker locker( &sMutex );
158 QSqlDatabase::removeDatabase( threadSafeConnectionName );
159 }, Qt::DirectConnection );
160 }
161 }
162 else
163 {
164 db = QSqlDatabase::database( threadSafeConnectionName );
165 }
166 locker.unlock();
167
168 db.setHostName( host );
169 QString connectionString;
170 if ( !service.isEmpty() )
171 {
172 // driver was specified explicitly
173 connectionString = service;
174 }
175 else
176 {
177 #ifdef Q_OS_WIN
178 connectionString = "driver={SQL Server}";
179 #elif defined (Q_OS_MAC)
180 QString freeTDSDriver( QCoreApplication::applicationDirPath().append( "/lib/libtdsodbc.so" ) );
181 if ( QFile::exists( freeTDSDriver ) )
182 {
183 connectionString = QStringLiteral( "driver=%1;port=1433;TDS_Version=auto" ).arg( freeTDSDriver );
184 }
185 else
186 {
187 connectionString = QStringLiteral( "driver={FreeTDS};port=1433;TDS_Version=auto" );
188 }
189 #else
190 // It seems that FreeTDS driver by default uses an ancient TDS protocol version (4.2) to communicate with MS SQL
191 // which was causing various data corruption errors, for example:
192 // - truncating data from varchar columns to 255 chars - failing to read WKT for CRS
193 // - truncating binary data to 4096 bytes (see @@TEXTSIZE) - failing to parse larger geometries
194 // The added "TDS_Version=auto" should negotiate more recent version (manually setting e.g. 7.2 worked fine too)
195 connectionString = QStringLiteral( "driver={FreeTDS};port=1433;TDS_Version=auto" );
196 #endif
197 }
198
199 if ( !host.isEmpty() )
200 connectionString += ";server=" + host;
201
202 if ( !database.isEmpty() )
203 connectionString += ";database=" + database;
204
205 if ( password.isEmpty() )
206 connectionString += QLatin1String( ";trusted_connection=yes" );
207 else
208 connectionString += ";uid=" + username + ";pwd=" + password;
209
210 if ( !username.isEmpty() )
211 db.setUserName( username );
212
213 if ( !password.isEmpty() )
214 db.setPassword( password );
215
216 db.setDatabaseName( connectionString );
217
218 // only uncomment temporarily -- it can show connection password otherwise!
219 // QgsDebugMsg( connectionString );
220 return db;
221 }
222