diff --git a/src/core/qgsconnectionpool.h b/src/core/qgsconnectionpool.h index 0e49bc2adf8..5b0fa1caa3f 100644 --- a/src/core/qgsconnectionpool.h +++ b/src/core/qgsconnectionpool.h @@ -285,8 +285,6 @@ class QgsConnectionPool * If \a timeout is a negative value the calling thread will be blocked * until a connection becomes available. This is the default behavior. * - * - * * \returns initialized connection or NULLPTR if unsuccessful */ T acquireConnection( const QString &connInfo, int timeout = -1, bool requestMayBeNested = false ) diff --git a/src/core/qgsspatialiteutils.cpp b/src/core/qgsspatialiteutils.cpp index c5f79b2187a..e9e1950b149 100644 --- a/src/core/qgsspatialiteutils.cpp +++ b/src/core/qgsspatialiteutils.cpp @@ -22,6 +22,22 @@ #include #include +// Define this variable to print all spatialite SQL statements +#ifdef SPATIALITE_PRINT_ALL_SQL +// Debugging code +#include +#include +static int trace_callback( unsigned, void *ctx, void *p, void * ) +{ + sqlite3_stmt *stmt = ( sqlite3_stmt * )p; + char *sql = sqlite3_expanded_sql( stmt ); + qDebug() << "SPATIALITE" << QThread::currentThreadId() << ( sqlite3 * ) ctx << sql; + sqlite3_free( sql ); + return 0; +} +#endif + + int spatialite_database_unique_ptr::open( const QString &path ) { auto &deleter = get_deleter(); @@ -54,6 +70,16 @@ int spatialite_database_unique_ptr::open_v2( const QString &path, int flags, con if ( result == SQLITE_OK ) spatialite_init_ex( database, deleter.mSpatialiteContext, 0 ); +#ifdef SPATIALITE_PRINT_ALL_SQL + // Log all queries + sqlite3_trace_v2( + database, + SQLITE_TRACE_STMT, + trace_callback, + database + ); +#endif + return result; } diff --git a/src/providers/spatialite/CMakeLists.txt b/src/providers/spatialite/CMakeLists.txt index 8f282ee45f2..9b78d7e8e51 100644 --- a/src/providers/spatialite/CMakeLists.txt +++ b/src/providers/spatialite/CMakeLists.txt @@ -13,6 +13,7 @@ SET(SPATIALITE_SRCS qgsspatialitefeatureiterator.cpp qgsspatialitetablemodel.cpp qgsspatialiteproviderconnection.cpp + qgsspatialitetransaction.cpp ) IF (WITH_GUI) diff --git a/src/providers/spatialite/qgsspatialiteconnection.h b/src/providers/spatialite/qgsspatialiteconnection.h index 08b34cb0792..328b935c409 100644 --- a/src/providers/spatialite/qgsspatialiteconnection.h +++ b/src/providers/spatialite/qgsspatialiteconnection.h @@ -158,7 +158,13 @@ class QgsSqliteHandle { mIsValid = false; } + + /** + * Returns a possibly cached SQLite DB object from \a path, if \a shared is FALSE + * the DB will not be searched in the cache and a new READ ONLY connection will be returned. + */ static QgsSqliteHandle *openDb( const QString &dbPath, bool shared = true ); + static bool checkMetadata( sqlite3 *handle ); static void closeDb( QgsSqliteHandle *&handle ); diff --git a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp index 1dbc84961dc..407b4c677ce 100644 --- a/src/providers/spatialite/qgsspatialitefeatureiterator.cpp +++ b/src/providers/spatialite/qgsspatialitefeatureiterator.cpp @@ -29,7 +29,16 @@ QgsSpatiaLiteFeatureIterator::QgsSpatiaLiteFeatureIterator( QgsSpatiaLiteFeatureSource *source, bool ownSource, const QgsFeatureRequest &request ) : QgsAbstractFeatureIteratorFromSource( source, ownSource, request ) { - mHandle = QgsSpatiaLiteConnPool::instance()->acquireConnection( mSource->mSqlitePath, request.timeout(), request.requestMayBeNested() ); + + mSqliteHandle = source->transactionHandle(); + if ( ! mSqliteHandle ) + { + mHandle = QgsSpatiaLiteConnPool::instance()->acquireConnection( mSource->mSqlitePath, request.timeout(), request.requestMayBeNested() ); + if ( mHandle ) + { + mSqliteHandle = mHandle->handle(); + } + } mFetchGeometry = !mSource->mGeometryColumn.isNull() && !( mRequest.flags() & QgsFeatureRequest::NoGeometry ); mHasPrimaryKey = !mSource->mPrimaryKey.isEmpty(); @@ -290,9 +299,10 @@ bool QgsSpatiaLiteFeatureIterator::close() iteratorClosed(); - if ( !mHandle ) + mClosed = true; + + if ( !mSqliteHandle ) { - mClosed = true; return false; } @@ -302,19 +312,24 @@ bool QgsSpatiaLiteFeatureIterator::close() sqliteStatement = nullptr; } - QgsSpatiaLiteConnPool::instance()->releaseConnection( mHandle ); - mHandle = nullptr; + if ( mHandle ) + { + QgsSpatiaLiteConnPool::instance()->releaseConnection( mHandle ); + mHandle = nullptr; + } + mSqliteHandle = nullptr; mClosed = true; return true; } + //// bool QgsSpatiaLiteFeatureIterator::prepareStatement( const QString &whereClause, long limit, const QString &orderBy ) { - if ( !mHandle ) + if ( !mSqliteHandle ) return false; try @@ -359,10 +374,10 @@ bool QgsSpatiaLiteFeatureIterator::prepareStatement( const QString &whereClause, QgsDebugMsgLevel( sql, 4 ); - if ( sqlite3_prepare_v2( mHandle->handle(), sql.toUtf8().constData(), -1, &sqliteStatement, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &sqliteStatement, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mHandle->handle() ) ), QObject::tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( QObject::tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), QObject::tr( "SpatiaLite" ) ); return false; } } @@ -489,7 +504,7 @@ bool QgsSpatiaLiteFeatureIterator::getFeature( sqlite3_stmt *stmt, QgsFeature &f if ( ret != SQLITE_ROW ) { // some unexpected error occurred - QgsMessageLog::logMessage( QObject::tr( "SQLite error getting feature: %1" ).arg( QString::fromUtf8( sqlite3_errmsg( mHandle->handle() ) ) ), QObject::tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( QObject::tr( "SQLite error getting feature: %1" ).arg( QString::fromUtf8( sqlite3_errmsg( mSqliteHandle ) ) ), QObject::tr( "SpatiaLite" ) ); return false; } @@ -642,6 +657,7 @@ QgsSpatiaLiteFeatureSource::QgsSpatiaLiteFeatureSource( const QgsSpatiaLiteProvi , mSpatialIndexMbrCache( p->mSpatialIndexMbrCache ) , mSqlitePath( p->mSqlitePath ) , mCrs( p->crs() ) + , mTransactionHandle( p->transaction() ? p->sqliteHandle() : nullptr ) { } @@ -649,3 +665,8 @@ QgsFeatureIterator QgsSpatiaLiteFeatureSource::getFeatures( const QgsFeatureRequ { return QgsFeatureIterator( new QgsSpatiaLiteFeatureIterator( this, false, request ) ); } + +sqlite3 *QgsSpatiaLiteFeatureSource::transactionHandle() +{ + return mTransactionHandle; +} diff --git a/src/providers/spatialite/qgsspatialitefeatureiterator.h b/src/providers/spatialite/qgsspatialitefeatureiterator.h index c1b98062596..57e49e90c9e 100644 --- a/src/providers/spatialite/qgsspatialitefeatureiterator.h +++ b/src/providers/spatialite/qgsspatialitefeatureiterator.h @@ -34,6 +34,8 @@ class QgsSpatiaLiteFeatureSource final: public QgsAbstractFeatureSource QgsFeatureIterator getFeatures( const QgsFeatureRequest &request ) override; + sqlite3 *transactionHandle(); + private: QString mGeometryColumn; QString mSubsetString; @@ -49,6 +51,7 @@ class QgsSpatiaLiteFeatureSource final: public QgsAbstractFeatureSource bool mSpatialIndexMbrCache; QString mSqlitePath; QgsCoordinateReferenceSystem mCrs; + sqlite3 *mTransactionHandle = nullptr; friend class QgsSpatiaLiteFeatureIterator; friend class QgsSpatiaLiteExpressionCompiler; @@ -81,8 +84,10 @@ class QgsSpatiaLiteFeatureIterator final: public QgsAbstractFeatureIteratorFromS QVariant getFeatureAttribute( sqlite3_stmt *stmt, int ic, QVariant::Type type, QVariant::Type subType ); void getFeatureGeometry( sqlite3_stmt *stmt, int ic, QgsFeature &feature ); - //! wrapper of the SQLite database connection + //! QGIS wrapper of the SQLite database connection QgsSqliteHandle *mHandle = nullptr; + //! The low level connection + sqlite3 *mSqliteHandle = nullptr; /** * SQLite statement handle diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index 3d2e9b9e486..6cad148619c 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -31,6 +31,7 @@ email : a.furieri@lqt.it #include "qgsfeedback.h" #include "qgsspatialitedataitems.h" #include "qgsspatialiteconnection.h" +#include "qgsspatialitetransaction.h" #include "qgsspatialiteproviderconnection.h" #include "qgsjsonutils.h" @@ -51,7 +52,7 @@ const QString QgsSpatiaLiteProvider::SPATIALITE_KEY = QStringLiteral( "spatialit const QString QgsSpatiaLiteProvider::SPATIALITE_DESCRIPTION = QStringLiteral( "SpatiaLite data provider" ); static const QString SPATIALITE_ARRAY_PREFIX = QStringLiteral( "json" ); static const QString SPATIALITE_ARRAY_SUFFIX = QStringLiteral( "list" ); - +QAtomicInt QgsSpatiaLiteProvider::sSavepointId = 0; bool QgsSpatiaLiteProvider::convertField( QgsField &field ) { @@ -459,8 +460,9 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider mPrimaryKey = anUri.keyColumn(); mQuery = mTableName; - // trying to open the SQLite DB + // Retrieve a shared connection mHandle = QgsSqliteHandle::openDb( mSqlitePath ); + if ( !mHandle ) { return; @@ -474,7 +476,7 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider for ( const auto &pragma : pragmaList ) { char *errMsg = nullptr; - int ret = sqlite3_exec( mSqliteHandle, ( "PRAGMA " + pragma ).toUtf8(), nullptr, nullptr, &errMsg ); + int ret = exec_sql( QStringLiteral( "PRAGMA %1" ).arg( pragma ), errMsg ); if ( ret != SQLITE_OK ) { QgsDebugMsg( QStringLiteral( "PRAGMA " ) + pragma + QString( " failed : %1" ).arg( errMsg ? errMsg : "" ) ); @@ -539,6 +541,7 @@ QgsSpatiaLiteProvider::QgsSpatiaLiteProvider( QString const &uri, const Provider mEnabledCapabilities |= QgsVectorDataProvider::AddFeatures; mEnabledCapabilities |= QgsVectorDataProvider::AddAttributes; mEnabledCapabilities |= QgsVectorDataProvider::CreateAttributeIndex; + mEnabledCapabilities |= QgsVectorDataProvider::TransactionSupport; } if ( lyr ) @@ -880,7 +883,7 @@ void QgsSpatiaLiteProvider::fetchConstraints() int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } @@ -929,7 +932,7 @@ void QgsSpatiaLiteProvider::fetchConstraints() int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } @@ -1036,7 +1039,7 @@ QString QgsSpatiaLiteProvider::defaultValueClause( int fieldIndex ) const return mDefaultValueClause.value( fieldIndex, QString() ); } -void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, bool rollback ) +void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, const QString &savepointId ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errorMessage ? errorMessage : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); // unexpected error @@ -1045,13 +1048,24 @@ void QgsSpatiaLiteProvider::handleError( const QString &sql, char *errorMessage, sqlite3_free( errorMessage ); } - if ( rollback ) + if ( ! savepointId.isEmpty() ) { // ROLLBACK after some previous error - ( void )sqlite3_exec( mSqliteHandle, "ROLLBACK", nullptr, nullptr, nullptr ); + ( void )exec_sql( QStringLiteral( "ROLLBACK TRANSACTION TO \"%1\"" ).arg( savepointId ) ); } } +int QgsSpatiaLiteProvider::exec_sql( const QString &sql, char *errMsg ) +{ + // Use transaction's handle (if any) + return sqlite3_exec( sqliteHandle(), sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); +} + +sqlite3 *QgsSpatiaLiteProvider::sqliteHandle() const +{ + return mTransaction && mTransaction->sqliteHandle() ? mTransaction->sqliteHandle() : mSqliteHandle; +} + void QgsSpatiaLiteProvider::loadFields() { int ret; @@ -1075,10 +1089,10 @@ void QgsSpatiaLiteProvider::loadFields() sql = QStringLiteral( "PRAGMA table_info(%1)" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return; } if ( rows < 1 ) @@ -1135,10 +1149,10 @@ void QgsSpatiaLiteProvider::loadFields() { sql = QStringLiteral( "select * from %1 limit 1" ).arg( mQuery ); - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); return; } @@ -1206,7 +1220,7 @@ void QgsSpatiaLiteProvider::determineViewPrimaryKey() int rows; int columns; char *errMsg = nullptr; - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { if ( rows > 0 ) @@ -1229,15 +1243,15 @@ QStringList QgsSpatiaLiteProvider::tablePrimaryKeys( const QString &tableName ) int rows; int columns; char *errMsg = nullptr; - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { for ( int row = 1; row <= rows; ++row ) @@ -1275,7 +1289,7 @@ bool QgsSpatiaLiteProvider::hasTriggers() sql = QStringLiteral( "SELECT * FROM sqlite_master WHERE type='trigger' AND tbl_name=%1" ) .arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); sqlite3_free_table( results ); return ( ret == SQLITE_OK && rows > 0 ); } @@ -1288,7 +1302,7 @@ bool QgsSpatiaLiteProvider::hasRowid() // table without rowid column QString sql = QStringLiteral( "SELECT rowid FROM %1 WHERE 0" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); char *errMsg = nullptr; - return sqlite3_exec( mSqliteHandle, sql.toUtf8(), nullptr, nullptr, &errMsg ) == SQLITE_OK; + return exec_sql( sql, errMsg ) == SQLITE_OK; } @@ -3719,7 +3733,7 @@ QVariant QgsSpatiaLiteProvider::minimumValue( int index ) const sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errMsg ? errMsg : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); @@ -3782,7 +3796,7 @@ QVariant QgsSpatiaLiteProvider::maximumValue( int index ) const sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errMsg ? errMsg : tr( "unknown cause" ) ), tr( "SpatiaLite" ) ); @@ -3852,10 +3866,10 @@ QSet QgsSpatiaLiteProvider::uniqueValues( int index, int limit ) const } // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { @@ -3891,7 +3905,7 @@ QSet QgsSpatiaLiteProvider::uniqueValues( int index, int limit ) const } else { - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); sqlite3_finalize( stmt ); return uniqueValues; } @@ -3932,10 +3946,10 @@ QStringList QgsSpatiaLiteProvider::uniqueStringsMatching( int index, const QStri } // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else @@ -3965,7 +3979,7 @@ QStringList QgsSpatiaLiteProvider::uniqueStringsMatching( int index, const QStri } else { - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); sqlite3_finalize( stmt ); return results; } @@ -4020,7 +4034,9 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) QgsAttributes attributevec = flist[0].attributes(); - ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret == SQLITE_OK ) { toCommit = true; @@ -4078,7 +4094,7 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) sql += ')'; // SQLite prepared statement - ret = sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ); + ret = sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ); if ( ret == SQLITE_OK ) { @@ -4185,14 +4201,14 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) // update feature id if ( !( flags & QgsFeatureSink::FastInsert ) ) { - feature->setId( sqlite3_last_insert_rowid( mSqliteHandle ) ); + feature->setId( sqlite3_last_insert_rowid( sqliteHandle( ) ) ); } mNumberFeatures++; } else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); break; @@ -4202,7 +4218,7 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) if ( ret == SQLITE_DONE || ret == SQLITE_ROW ) { - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); } } // BEGIN statement @@ -4218,9 +4234,14 @@ bool QgsSpatiaLiteProvider::addFeatures( QgsFeatureList &flist, Flags flags ) if ( toCommit ) { // ROLLBACK after some previous error - ( void )sqlite3_exec( mSqliteHandle, "ROLLBACK", nullptr, nullptr, nullptr ); + ( void )exec_sql( QStringLiteral( "ROLLBACK TRANSACTION TO SAVEPOINT \"%1\"" ).arg( savepointId ) ); } } + else + { + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + } return ret == SQLITE_OK; } @@ -4243,10 +4264,12 @@ bool QgsSpatiaLiteProvider::createAttributeIndex( int field ) QString sql; QString fieldName; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4256,20 +4279,23 @@ bool QgsSpatiaLiteProvider::createAttributeIndex( int field ) .arg( createIndexName( mTableName, fieldName ), mTableName, QgsSqliteUtils::quotedIdentifier( fieldName ) ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4293,20 +4319,22 @@ bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } sql = QStringLiteral( "DELETE FROM %1 WHERE %2=?" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ), QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ); // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - pushError( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ) ); + pushError( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ) ); return false; } else @@ -4330,10 +4358,10 @@ bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } @@ -4341,13 +4369,16 @@ bool QgsSpatiaLiteProvider::deleteFeatures( const QgsFeatureIds &id ) sqlite3_finalize( stmt ); - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4356,28 +4387,33 @@ bool QgsSpatiaLiteProvider::truncate() char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } sql = QStringLiteral( "DELETE FROM %1" ).arg( QgsSqliteUtils::quotedIdentifier( mTableName ) ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4389,10 +4425,12 @@ bool QgsSpatiaLiteProvider::addAttributes( const QList &attributes ) if ( attributes.isEmpty() ) return true; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4402,27 +4440,30 @@ bool QgsSpatiaLiteProvider::addAttributes( const QList &attributes ) .arg( mTableName, iter->name(), iter->typeName() ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql, errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } - gaiaStatisticsInvalidate( mSqliteHandle, mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); - update_layer_statistics( mSqliteHandle, mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); + gaiaStatisticsInvalidate( sqliteHandle( ), mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); + update_layer_statistics( sqliteHandle( ), mTableName.toUtf8().constData(), mGeometryColumn.toUtf8().constData() ); // reload columns loadFields(); + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4434,10 +4475,12 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap if ( attr_map.isEmpty() ) return true; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4504,7 +4547,7 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap auto msgPtr { static_cast( sqlite3_malloc( errM.length() + 1 ) ) }; strcpy( static_cast( msgPtr ), errM.toStdString().c_str() ); errMsg = msgPtr; - handleError( jRepr, errMsg, true ); + handleError( jRepr, errMsg, savepointId ); return false; } } @@ -4521,21 +4564,24 @@ bool QgsSpatiaLiteProvider::changeAttributeValues( const QgsChangedAttributesMap } sql += QStringLiteral( " WHERE %1=%2" ).arg( QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ).arg( fid ); - ret = sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ); + ret = exec_sql( sql.toUtf8(), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4545,10 +4591,12 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry char *errMsg = nullptr; QString sql; - int ret = sqlite3_exec( mSqliteHandle, "BEGIN", nullptr, nullptr, &errMsg ); + const QString savepointId { QStringLiteral( "qgis_spatialite_internal_savepoint_%1" ).arg( ++ sSavepointId ) }; + + int ret = exec_sql( QStringLiteral( "SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -4560,10 +4608,10 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry .arg( QgsSqliteUtils::quotedIdentifier( mPrimaryKey ) ); // SQLite prepared statement - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) != SQLITE_OK ) { // some error occurred - QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( mSqliteHandle ) ), tr( "SpatiaLite" ) ); + QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, sqlite3_errmsg( sqliteHandle( ) ) ), tr( "SpatiaLite" ) ); } else { @@ -4591,10 +4639,10 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry else { // some unexpected error occurred - const char *err = sqlite3_errmsg( mSqliteHandle ); + const char *err = sqlite3_errmsg( sqliteHandle( ) ); errMsg = ( char * ) sqlite3_malloc( ( int ) strlen( err ) + 1 ); strcpy( errMsg, err ); - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } } @@ -4602,12 +4650,16 @@ bool QgsSpatiaLiteProvider::changeGeometryValues( const QgsGeometryMap &geometry sqlite3_finalize( stmt ); - ret = sqlite3_exec( mSqliteHandle, "COMMIT", nullptr, nullptr, &errMsg ); + ret = exec_sql( QStringLiteral( "RELEASE SAVEPOINT \"%1\"" ).arg( savepointId ), errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg, true ); + handleError( sql, errMsg, savepointId ); return false; } + + if ( mTransaction ) + mTransaction->dirtyLastSavePoint(); + return true; } @@ -4706,7 +4758,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() "WHERE lower(name) = lower(%1) " "AND type in ('table', 'view') " ).arg( QgsSqliteUtils::quotedString( mTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { QString type = QString( results[ columns + 0 ] ); @@ -4751,7 +4803,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() QgsSqliteUtils::quotedIdentifier( alias ) ); sql = QStringLiteral( "SELECT 0, %1 FROM %2 LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); // Try to find a PK or try to use ROWID if ( ret == SQLITE_OK && rows == 1 ) @@ -4761,7 +4813,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() // 1. find the table that provides geometry // String containing the name of the table that provides the geometry if the layer data source is based on a query QString queryGeomTableName; - if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) == SQLITE_OK ) + if ( sqlite3_prepare_v2( sqliteHandle( ), sql.toUtf8().constData(), -1, &stmt, nullptr ) == SQLITE_OK ) { queryGeomTableName = sqlite3_column_table_name( stmt, 1 ); } @@ -4815,7 +4867,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() // Try first without any injection or manipulation sql = QStringLiteral( "SELECT %1, %2 FROM %3 LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( pks.first( ) ), QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mPrimaryKey = pks.first( ); @@ -4828,7 +4880,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedIdentifier( tableIdentifier ) ) .arg( pks.first() ) ) ); sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 1" ).arg( pk ).arg( newSql ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mQuery = newSql; @@ -4844,7 +4896,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() if ( ! queryGeomTableName.isEmpty() ) { sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( QgsSqliteUtils::quotedIdentifier( queryGeomTableName ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK || rows != 1 ) { queryGeomTableName = QString(); @@ -4858,7 +4910,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedIdentifier( tableIdentifier ), QStringLiteral( "ROWID" ) ) ) ); sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( newSql ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mQuery = newSql; @@ -4897,7 +4949,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() .arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { if ( errMsg && strcmp( errMsg, "no such table: geometry_columns_auth" ) == 0 ) @@ -4906,7 +4958,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() sql = QStringLiteral( "SELECT 0 FROM geometry_columns WHERE upper(f_table_name) = upper(%1) and upper(f_geometry_column) = upper(%2)" ) .arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); } } if ( ret == SQLITE_OK && rows == 1 ) @@ -4936,7 +4988,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() " WHERE view_name=%1 and view_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mViewBased = true; @@ -4956,7 +5008,7 @@ bool QgsSpatiaLiteProvider::checkLayerType() " WHERE virt_name=%1 and virt_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK && rows == 1 ) { mVShapeBased = true; @@ -5066,10 +5118,10 @@ void QgsSpatiaLiteProvider::getViewSpatialIndexName() "FROM views_geometry_columns " "WHERE upper(view_name) = upper(%1) and upper(view_geometry) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); } if ( rows < 1 ) ; @@ -5118,7 +5170,7 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() mIndexGeometry = mGeometryColumn; QString sql; - if ( ! versionIsAbove( mSqliteHandle, 3, 1 ) ) + if ( ! versionIsAbove( sqliteHandle( ), 3, 1 ) ) { sql = QString( "SELECT type, srid, spatial_index_enabled, coord_dimension FROM geometry_columns" " WHERE upper(f_table_name) = upper(%1) and upper(f_geometry_column) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), @@ -5131,10 +5183,10 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() QgsSqliteUtils::quotedString( mGeometryColumn ) ); } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5208,7 +5260,7 @@ bool QgsSpatiaLiteProvider::getTableGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5230,10 +5282,10 @@ bool QgsSpatiaLiteProvider::getViewGeometryDetails() " WHERE upper(view_name) = upper(%1) and upper(view_geometry) = upper(%2)" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5288,7 +5340,7 @@ bool QgsSpatiaLiteProvider::getViewGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5308,10 +5360,10 @@ bool QgsSpatiaLiteProvider::getVShapeGeometryDetails() " WHERE virt_name=%1 and virt_geometry=%2" ).arg( QgsSqliteUtils::quotedString( mTableName ), QgsSqliteUtils::quotedString( mGeometryColumn ) ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5355,7 +5407,7 @@ bool QgsSpatiaLiteProvider::getVShapeGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5376,7 +5428,7 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() // get stuff from the relevant column instead. This may (will?) // fail if there is no data in the relevant table. - QString sql = QStringLiteral( "select srid(%1), geometrytype(%1) from %2" ) + QString sql = QStringLiteral( "SELECT srid(%1), geometrytype(%1) FROM %2" ) .arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); @@ -5388,10 +5440,10 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() sql += QLatin1String( " limit 1" ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5412,23 +5464,23 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() if ( fType == QLatin1String( "GEOMETRY" ) ) { // check to see if there is a unique geometry type - sql = QString( "select distinct " - "case" - " when geometrytype(%1) IN ('POINT','MULTIPOINT') THEN 'POINT'" - " when geometrytype(%1) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" - " when geometrytype(%1) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" - " end " - "from %2" ) + sql = QString( "SELECT DISTINCT " + "CASE" + " WHEN geometrytype(%1) IN ('POINT','MULTIPOINT') THEN 'POINT'" + " WHEN geometrytype(%1) IN ('LINESTRING','MULTILINESTRING') THEN 'LINESTRING'" + " WHEN geometrytype(%1) IN ('POLYGON','MULTIPOLYGON') THEN 'POLYGON'" + " END " + "FROM %2" ) .arg( QgsSqliteUtils::quotedIdentifier( mGeometryColumn ), mQuery ); if ( !mSubsetString.isEmpty() ) sql += " where " + mSubsetString; - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5473,7 +5525,7 @@ bool QgsSpatiaLiteProvider::getQueryGeometryDetails() if ( mGeomType == QgsWkbTypes::Unknown || mSrid < 0 ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } @@ -5491,10 +5543,10 @@ bool QgsSpatiaLiteProvider::getSridDetails() QString sql = QStringLiteral( "SELECT auth_name||':'||auth_srid,proj4text FROM spatial_ref_sys WHERE srid=%1" ).arg( mSrid ); - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5550,10 +5602,10 @@ bool QgsSpatiaLiteProvider::getTableSummary() sql += " WHERE ( " + mSubsetString + ')'; } - ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret != SQLITE_OK ) { - handleError( sql, errMsg ); + handleError( sql, errMsg, QString() ); return false; } if ( rows < 1 ) @@ -5668,6 +5720,17 @@ QList QgsSpatiaLiteProvider::searchLayers( const QList( transaction ); +} + +QgsTransaction *QgsSpatiaLiteProvider::transaction( ) const +{ + return static_cast( mTransaction ); +} QList QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLayer *self, const QList &layers ) const { @@ -5677,7 +5740,7 @@ QList QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLaye int rows; int columns; char *errMsg = nullptr; - int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + int ret = sqlite3_get_table( sqliteHandle( ), sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); if ( ret == SQLITE_OK ) { int nbFound = 0; @@ -6140,7 +6203,21 @@ QList< QgsDataItemProvider * > QgsSpatiaLiteProviderMetadata::dataItemProviders( return providers; } - +QgsTransaction *QgsSpatiaLiteProviderMetadata::createTransaction( const QString &connString ) +{ + const QgsDataSourceUri dsUri{ connString }; + // Cannot use QgsSpatiaLiteConnPool::instance()->acquireConnection( dsUri.database() ) }; + // because it will return a read only connection, use the (cached) connection from the + // layers instead. + QgsSqliteHandle *ds { QgsSqliteHandle::openDb( dsUri.database() ) }; + if ( !ds ) + { + QgsMessageLog::logMessage( QObject::tr( "Cannot open transaction on %1, since it is is not currently opened" ).arg( connString ), + QObject::tr( "spatialite" ), Qgis::Critical ); + return nullptr; + } + return new QgsSpatiaLiteTransaction( connString, ds ); +} QMap QgsSpatiaLiteProviderMetadata::connections( bool cached ) { diff --git a/src/providers/spatialite/qgsspatialiteprovider.h b/src/providers/spatialite/qgsspatialiteprovider.h index 216780d5d9c..a9ec8c128bf 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.h +++ b/src/providers/spatialite/qgsspatialiteprovider.h @@ -43,6 +43,8 @@ class QgsField; class QgsSqliteHandle; class QgsSpatiaLiteFeatureIterator; +class QgsSpatiaLiteTransaction; +class QgsTransaction; #include "qgsdatasourceuri.h" @@ -220,6 +222,12 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider */ static QList searchLayers( const QList &layers, const QString &connectionInfo, const QString &tableName ); + QgsSpatiaLiteTransaction *mTransaction = nullptr; + + QgsTransaction *transaction() const override; + + void setTransaction( QgsTransaction *transaction ) override; + QgsFields mAttributeFields; //! Map of field index to default value SQL fragments @@ -325,6 +333,10 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider //! SpatiaLite minor version int mSpatialiteVersionMinor = 0; + //! Internal transaction handling (for addFeatures etc.) + int mSavepointId; + static QAtomicInt sSavepointId; + /** * internal utility functions used to handle common SQLite tasks */ @@ -382,7 +394,17 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider /** * Handles an error encountered while executing an sql statement. */ - void handleError( const QString &sql, char *errorMessage, bool rollback = false ); + void handleError( const QString &sql, char *errorMessage, const QString &savepointId ); + + /** + * Sqlite exec sql wrapper for SQL logging + */ + int exec_sql( const QString &sql, char *errMsg = nullptr ); + + /** + * Returns the sqlite handle to be used, if we are inside a transaction it will be the transaction's handle + */ + sqlite3 *sqliteHandle( ) const; friend class QgsSpatiaLiteFeatureSource; @@ -421,14 +443,14 @@ class QgsSpatiaLiteProviderMetadata final: public QgsProviderMetadata QgsAbstractProviderConnection *createConnection( const QString &name ) override; void deleteConnection( const QString &name ) override; void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) override; + QgsTransaction *createTransaction( const QString &connString ) override; protected: QgsAbstractProviderConnection *createConnection( const QString &uri, const QVariantMap &configuration ) override; - - }; + // clazy:excludeall=qstring-allocations #endif diff --git a/src/providers/spatialite/qgsspatialitetransaction.cpp b/src/providers/spatialite/qgsspatialitetransaction.cpp new file mode 100644 index 00000000000..4acade1b55d --- /dev/null +++ b/src/providers/spatialite/qgsspatialitetransaction.cpp @@ -0,0 +1,89 @@ +/*************************************************************************** + qgsspatialitetransaction.cpp - QgsSpatialiteTransaction + + --------------------- + begin : 30.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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 "qgsspatialitetransaction.h" +#include "qgslogger.h" +#include + +///@cond PRIVATE + +QAtomicInt QgsSpatiaLiteTransaction::sSavepointId = 0; + +QgsSpatiaLiteTransaction::QgsSpatiaLiteTransaction( const QString &connString, QgsSqliteHandle *sharedHandle ) + : QgsTransaction( connString ) + , mSharedHandle( sharedHandle ) +{ + if ( mSharedHandle ) + mSqliteHandle = mSharedHandle->handle(); + mSavepointId = ++sSavepointId; +} + +bool QgsSpatiaLiteTransaction::beginTransaction( QString &error, int /* statementTimeout */ ) +{ + return executeSql( QStringLiteral( "BEGIN" ), error ); +} + +bool QgsSpatiaLiteTransaction::commitTransaction( QString &error ) +{ + return executeSql( QStringLiteral( "COMMIT" ), error ); +} + +bool QgsSpatiaLiteTransaction::rollbackTransaction( QString &error ) +{ + return executeSql( QStringLiteral( "ROLLBACK" ), error ); +} + +bool QgsSpatiaLiteTransaction::executeSql( const QString &sql, QString &errorMsg, bool isDirty, const QString &name ) +{ + + if ( ! mSqliteHandle ) + { + QgsDebugMsg( QStringLiteral( "Spatialite handle is not set" ) ); + return false; + } + + QString err; + if ( isDirty ) + { + createSavepoint( err ); + } + char *errMsg = nullptr; + if ( sqlite3_exec( mSqliteHandle, sql.toUtf8().constData(), nullptr, nullptr, &errMsg ) != SQLITE_OK ) + { + QgsDebugMsg( errorMsg ); + if ( isDirty ) + { + rollbackToSavepoint( savePoints().last(), err ); + } + sqlite3_free( errMsg ); + return false; + } + + if ( isDirty ) + { + dirtyLastSavePoint(); + emit dirtied( sql, name ); + } + + QgsDebugMsg( QStringLiteral( "... ok" ) ); + return true; +} + +sqlite3 *QgsSpatiaLiteTransaction::sqliteHandle() const +{ + return mSqliteHandle; +} +///@endcond diff --git a/src/providers/spatialite/qgsspatialitetransaction.h b/src/providers/spatialite/qgsspatialitetransaction.h new file mode 100644 index 00000000000..daf8e767cb9 --- /dev/null +++ b/src/providers/spatialite/qgsspatialitetransaction.h @@ -0,0 +1,64 @@ +/*************************************************************************** + qgsspatialitetransaction.h - QgsSpatialiteTransaction + + --------------------- + begin : 30.3.2020 + copyright : (C) 2020 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * 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. * + * * + ***************************************************************************/ +#ifndef QGSSPATIALITETRANSACTION_H +#define QGSSPATIALITETRANSACTION_H + +#include "qgstransaction.h" +#include "qgsspatialiteconnection.h" +#include "qgis_sip.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsSpatiaLiteTransaction : public QgsTransaction +{ + Q_OBJECT + + public: + explicit QgsSpatiaLiteTransaction( const QString &connString, QgsSqliteHandle *sharedHandle ); + + /** + * Executes the SQL query in database. + * + * \param sql The SQL query to execute + * \param error The error or an empty string if none + * \param isDirty True to add an undo/redo command in the edition buffer, false otherwise + * \param name Name of the operation ( only used if `isDirty` is true) + */ + bool executeSql( const QString &sql, QString &error, bool isDirty = false, const QString &name = QString() ) override; + + /** + * Returns the (possibly NULL) sqlite handle + */ + sqlite3 *sqliteHandle() const; + + private: + + QgsSqliteHandle *mSharedHandle = nullptr; + int mSavepointId; + + //! SQLite handle + sqlite3 *mSqliteHandle = nullptr; + + bool beginTransaction( QString &error, int statementTimeout ) override; + bool commitTransaction( QString &error ) override; + bool rollbackTransaction( QString &error ) override; + + static QAtomicInt sSavepointId; +}; + +///@endcond +#endif // QGSSPATIALITETRANSACTION_H diff --git a/tests/src/python/test_provider_spatialite.py b/tests/src/python/test_provider_spatialite.py index 7f3890a609e..d1f9e56bb59 100644 --- a/tests/src/python/test_provider_spatialite.py +++ b/tests/src/python/test_provider_spatialite.py @@ -17,9 +17,11 @@ import re import sys import shutil import tempfile +from osgeo import ogr from datetime import datetime from qgis.core import (QgsProviderRegistry, + QgsDataSourceUri, QgsVectorLayer, QgsVectorDataProvider, QgsPointXY, @@ -1295,6 +1297,85 @@ class TestQgsSpatialiteProvider(unittest.TestCase, ProviderTestCase): self.assertEqual(vl.featureCount(), 1) self.assertEqual(vl.getFeature(1).geometry().asWkt().upper(), 'POINT (9 45)') + def testTransaction(self): + """Test spatialite transactions""" + + tmpfile = tempfile.mktemp('.db') + ds = ogr.GetDriverByName('SQLite').CreateDataSource(tmpfile, options=['SPATIALITE=YES']) + lyr = ds.CreateLayer('lyr1', geom_type=ogr.wkbPoint) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(0 1)')) + lyr.CreateFeature(f) + lyr = ds.CreateLayer('lyr2', geom_type=ogr.wkbPoint) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(2 3)')) + lyr.CreateFeature(f) + f = ogr.Feature(lyr.GetLayerDefn()) + f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(4 5)')) + lyr.CreateFeature(f) + ds = None + + uri1 = QgsDataSourceUri() + uri1.setDatabase(tmpfile) + uri1.setTable('lyr1') + uri2 = QgsDataSourceUri() + uri2.setDatabase(tmpfile) + uri2.setTable('lyr2') + + vl1 = QgsVectorLayer(uri1.uri(), 'test', 'spatialite') + self.assertTrue(vl1.isValid()) + vl2 = QgsVectorLayer(uri2.uri(), 'test', 'spatialite') + self.assertTrue(vl2.isValid()) + + # prepare a project with transactions enabled + p = QgsProject() + p.setAutoTransaction(True) + p.addMapLayers([vl1, vl2]) + + self.assertTrue(vl1.startEditing()) + self.assertIsNotNone(vl1.dataProvider().transaction()) + + self.assertTrue(vl1.deleteFeature(1)) + + # An iterator opened on the layer should see the feature deleted + self.assertEqual(len([f for f in vl1.getFeatures(QgsFeatureRequest())]), 0) + + # But not if opened from another connection + vl1_external = QgsVectorLayer(uri1.uri(), 'test', 'spatialite') + self.assertTrue(vl1_external.isValid()) + self.assertEqual(len([f for f in vl1_external.getFeatures(QgsFeatureRequest())]), 1) + del vl1_external + + self.assertTrue(vl1.commitChanges()) + + # Should still get zero features on vl1 + self.assertEqual(len([f for f in vl1.getFeatures(QgsFeatureRequest())]), 0) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 2) + + # Test undo/redo + self.assertTrue(vl2.startEditing()) + self.assertIsNotNone(vl2.dataProvider().transaction()) + self.assertTrue(vl2.editBuffer().deleteFeature(1)) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + self.assertTrue(vl2.editBuffer().deleteFeature(2)) + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 0) + vl2.undoStack().undo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + vl2.undoStack().undo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 2) + vl2.undoStack().redo() + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + self.assertTrue(vl2.commitChanges()) + + self.assertEqual(len([f for f in vl2.getFeatures(QgsFeatureRequest())]), 1) + del vl1 + del vl2 + + vl2_external = QgsVectorLayer(uri2.uri(), 'test', 'spatialite') + self.assertTrue(vl2_external.isValid()) + self.assertEqual(len([f for f in vl2_external.getFeatures(QgsFeatureRequest())]), 1) + del vl2_external + if __name__ == '__main__': unittest.main()