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