[OGR provider] [FEATURE] Add support for transactions on GPKG databases

For complete support, it requires two GDAL fixes:
- One to avoid feature count to be invalid when using ROLLBACK TO SAVEPOINT
  f73ec8cd1d

- Another one to avoid nasty issues, at least on Linux, with the POSIX
  advisory locks used by libsqlite that could be invalidated due to how GDAL
  could open files behind the back of libsqlite. The consequence of this
  could be the deletion of -wal and -shm files, which caused issues in QGIS
  (non working iterators when the edit is finished, and later edits in the
  same session not working). Those issues could appear for example if doing
  ogrinfo on the .gpkg opened by QGIS, or if opening two QGIS session on the
  .gpkg

Both fixes are queued for GDAL 2.3.1
This commit is contained in:
Even Rouault 2018-06-14 19:07:13 +02:00
parent 370bac9935
commit 6a987f913b
No known key found for this signature in database
GPG Key ID: 33EBBFC47B3DD87D
12 changed files with 573 additions and 68 deletions

View File

@ -146,6 +146,7 @@ returns the last created savepoint
.. versionadded:: 3.0
%End
signals:
void afterRollback();

View File

@ -51,7 +51,7 @@ QgsTransaction *QgsTransaction::create( const QSet<QgsVectorLayer *> &layers )
QgsVectorLayer *firstLayer = *layers.constBegin();
QString connStr = QgsDataSourceUri( firstLayer->source() ).connectionInfo( false );
QString connStr = connectionString( firstLayer->source() );
QString providerKey = firstLayer->dataProvider()->name();
std::unique_ptr<QgsTransaction> transaction( QgsTransaction::create( connStr, providerKey ) );
if ( transaction )
@ -81,6 +81,46 @@ QgsTransaction::~QgsTransaction()
setLayerTransactionIds( nullptr );
}
// For the needs of the OGR provider with GeoPackage datasources, remove
// any reference to layers in the connection string
QString QgsTransaction::removeLayerIdOrName( const QString &str )
{
QString res( str );
for ( int i = 0; i < 2; i++ )
{
int pos = res.indexOf( i == 0 ? QLatin1String( "|layername=" ) : QLatin1String( "|layerid=" ) );
if ( pos >= 0 )
{
int end = res.indexOf( '|', pos + 1 );
if ( end >= 0 )
{
res = res.mid( 0, pos ) + res.mid( end );
}
else
{
res = res.mid( 0, pos );
}
}
}
return res;
}
///@cond PRIVATE
QString QgsTransaction::connectionString( const QString &layerName )
{
QString connString = QgsDataSourceUri( layerName ).connectionInfo( false );
// In the case of a OGR datasource, connectionInfo() will return an empty
// string. In that case, use the layer->source() itself, and strip any
// reference to layers from it.
if ( connString.isEmpty() )
{
connString = removeLayerIdOrName( layerName );
}
return connString;
}
///@endcond
bool QgsTransaction::addLayer( QgsVectorLayer *layer )
{
if ( !layer )
@ -90,17 +130,18 @@ bool QgsTransaction::addLayer( QgsVectorLayer *layer )
return false;
//test if provider supports transactions
if ( !layer->dataProvider() || ( layer->dataProvider()->capabilities() & QgsVectorDataProvider::TransactionSupport ) == 0 )
if ( !supportsTransaction( layer ) )
return false;
if ( layer->dataProvider()->transaction() )
return false;
//connection string not compatible
if ( QgsDataSourceUri( layer->source() ).connectionInfo( false ) != mConnString )
if ( connectionString( layer->source() ) != mConnString )
{
QgsDebugMsg( QString( "Couldn't start transaction because connection string for layer %1 : '%2' does not match '%3'" ).arg(
layer->id(), QgsDataSourceUri( layer->source() ).connectionInfo( false ), mConnString ) );
layer->id(), connectionString( layer->source() ), mConnString ) );
return false;
}
@ -162,11 +203,11 @@ bool QgsTransaction::rollback( QString &errorMsg )
bool QgsTransaction::supportsTransaction( const QgsVectorLayer *layer )
{
std::unique_ptr< QLibrary > lib( QgsProviderRegistry::instance()->createProviderLibrary( layer->providerType() ) );
if ( !lib )
//test if provider supports transactions
if ( !layer->dataProvider() || ( layer->dataProvider()->capabilities() & QgsVectorDataProvider::TransactionSupport ) == 0 )
return false;
return lib->resolve( "createTransaction" );
return true;
}
void QgsTransaction::onLayerDeleted()

View File

@ -158,6 +158,11 @@ class CORE_EXPORT QgsTransaction : public QObject SIP_ABSTRACT
*/
bool lastSavePointIsDirty() const { return mLastSavePointIsDirty; }
///@cond PRIVATE
// For internal use only, or by QgsTransactionGroup
static QString connectionString( const QString &layerName ) SIP_SKIP;
///@endcond
signals:
/**
@ -188,6 +193,8 @@ class CORE_EXPORT QgsTransaction : public QObject SIP_ABSTRACT
void setLayerTransactionIds( QgsTransaction *transaction );
static QString removeLayerIdOrName( const QString &str );
virtual bool beginTransaction( QString &error, int statementTimeout ) = 0;
virtual bool commitTransaction( QString &error ) = 0;
virtual bool rollbackTransaction( QString &error ) = 0;

View File

@ -33,8 +33,7 @@ bool QgsTransactionGroup::addLayer( QgsVectorLayer *layer )
if ( !QgsTransaction::supportsTransaction( layer ) )
return false;
QString connString = QgsDataSourceUri( layer->source() ).connectionInfo();
QString connString = QgsTransaction::connectionString( layer->source() );
if ( mConnString.isEmpty() )
{
mConnString = connString;
@ -74,6 +73,8 @@ void QgsTransactionGroup::onEditingStarted()
return;
mTransaction.reset( QgsTransaction::create( mConnString, mProviderKey ) );
if ( !mTransaction )
return;
QString errorMsg;
mTransaction->begin( errorMsg );

View File

@ -9,6 +9,7 @@ SET (OGR_SRCS
qgsgeopackagerasterwritertask.cpp
qgsogrdbconnection.cpp
qgsogrdbtablemodel.cpp
qgsogrtransaction.cpp
)
SET(OGR_MOC_HDRS
@ -19,6 +20,7 @@ SET(OGR_MOC_HDRS
qgsgeopackagerasterwritertask.h
qgsogrdbconnection.h
qgsogrdbtablemodel.h
qgsogrtransaction.h
)
IF (WITH_GUI)

View File

@ -26,6 +26,7 @@
#include "qgssettings.h"
#include "qgsexception.h"
#include "qgswkbtypes.h"
#include "qgsogrtransaction.h"
#include <QTextCodec>
#include <QFile>
@ -42,38 +43,51 @@ QgsOgrFeatureIterator::QgsOgrFeatureIterator( QgsOgrFeatureSource *source, bool
: QgsAbstractFeatureIteratorFromSource<QgsOgrFeatureSource>( source, ownSource, request )
, mFilterFids( mRequest.filterFids() )
, mFilterFidsIt( mFilterFids.constBegin() )
, mSharedDS( source->mSharedDS )
{
//QgsDebugMsg( "Feature iterator of " + mSource->mLayerName + ": acquiring connection");
mConn = QgsOgrConnPool::instance()->acquireConnection( QgsOgrProviderUtils::connectionPoolId( mSource->mDataSource ) );
if ( !mConn->ds )
if ( mSharedDS )
{
return;
}
if ( mSource->mLayerName.isNull() )
{
mOgrLayer = GDALDatasetGetLayer( mConn->ds, mSource->mLayerIndex );
}
else
{
mOgrLayer = GDALDatasetGetLayerByName( mConn->ds, mSource->mLayerName.toUtf8().constData() );
}
if ( !mOgrLayer )
{
return;
}
if ( !mSource->mSubsetString.isEmpty() )
{
mOgrOrigLayer = mOgrLayer;
mOgrLayerWithFid = QgsOgrProviderUtils::setSubsetString( mOgrLayer, mConn->ds, mSource->mEncoding, QString(), true, &mOrigFidAdded );
mOgrLayer = QgsOgrProviderUtils::setSubsetString( mOgrLayer, mConn->ds, mSource->mEncoding, mSource->mSubsetString, true, &mOrigFidAdded );
mOgrLayer = mSharedDS->createSQLResultLayer( mSource->mEncoding, mSource->mLayerName, mSource->mLayerIndex );
if ( !mOgrLayer )
{
close();
return;
}
}
else
{
//QgsDebugMsg( "Feature iterator of " + mSource->mLayerName + ": acquiring connection");
mConn = QgsOgrConnPool::instance()->acquireConnection( QgsOgrProviderUtils::connectionPoolId( mSource->mDataSource ) );
if ( !mConn->ds )
{
return;
}
if ( mSource->mLayerName.isNull() )
{
mOgrLayer = GDALDatasetGetLayer( mConn->ds, mSource->mLayerIndex );
}
else
{
mOgrLayer = GDALDatasetGetLayerByName( mConn->ds, mSource->mLayerName.toUtf8().constData() );
}
if ( !mOgrLayer )
{
return;
}
if ( !mSource->mSubsetString.isEmpty() )
{
mOgrOrigLayer = mOgrLayer;
mOgrLayerWithFid = QgsOgrProviderUtils::setSubsetString( mOgrLayer, mConn->ds, mSource->mEncoding, QString(), true, &mOrigFidAdded );
mOgrLayer = QgsOgrProviderUtils::setSubsetString( mOgrLayer, mConn->ds, mSource->mEncoding, mSource->mSubsetString, true, &mOrigFidAdded );
if ( !mOgrLayer )
{
close();
return;
}
}
}
QMutexLocker locker( mSharedDS ? &mSharedDS->mutex() : nullptr );
if ( mRequest.destinationCrs().isValid() && mRequest.destinationCrs() != mSource->mCrs )
{
@ -237,6 +251,8 @@ bool QgsOgrFeatureIterator::fetchFeatureWithId( QgsFeatureId id, QgsFeature &fea
bool QgsOgrFeatureIterator::fetchFeature( QgsFeature &feature )
{
QMutexLocker locker( mSharedDS ? &mSharedDS->mutex() : nullptr );
feature.setValid( false );
if ( mClosed || !mOgrLayer )
@ -286,6 +302,8 @@ bool QgsOgrFeatureIterator::fetchFeature( QgsFeature &feature )
bool QgsOgrFeatureIterator::rewind()
{
QMutexLocker locker( mSharedDS ? &mSharedDS->mutex() : nullptr );
if ( mClosed || !mOgrLayer )
return false;
@ -299,6 +317,20 @@ bool QgsOgrFeatureIterator::rewind()
bool QgsOgrFeatureIterator::close()
{
if ( mSharedDS )
{
iteratorClosed();
if ( mOgrLayer )
{
mSharedDS->releaseResultSet( mOgrLayer );
mOgrLayer = nullptr;
}
mSharedDS.reset();
mClosed = true;
return true;
}
if ( !mConn )
return false;
@ -444,7 +476,12 @@ QgsOgrFeatureSource::QgsOgrFeatureSource( const QgsOgrProvider *p )
, mDriverName( p->mGDALDriverName )
, mCrs( p->crs() )
, mWkbType( p->wkbType() )
, mSharedDS( nullptr )
{
if ( p->mTransaction )
{
mSharedDS = p->mTransaction->sharedDS();
}
for ( int i = ( p->mFirstFieldIsFid ) ? 1 : 0; i < mFields.size(); i++ )
mFieldsWithoutFid.append( mFields.at( i ) );
QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( mDataSource ) );

View File

@ -21,8 +21,12 @@
#include <ogr_api.h>
#include <memory>
class QgsOgrFeatureIterator;
class QgsOgrProvider;
class QgsOgrDataset;
using QgsOgrDatasetSharedPtr = std::shared_ptr< QgsOgrDataset>;
class QgsOgrFeatureSource : public QgsAbstractFeatureSource
{
@ -45,6 +49,7 @@ class QgsOgrFeatureSource : public QgsAbstractFeatureSource
QString mDriverName;
QgsCoordinateReferenceSystem mCrs;
QgsWkbTypes::Type mWkbType = QgsWkbTypes::Unknown;
QgsOgrDatasetSharedPtr mSharedDS = nullptr;
friend class QgsOgrFeatureIterator;
friend class QgsOgrExpressionCompiler;
@ -87,6 +92,7 @@ class QgsOgrFeatureIterator : public QgsAbstractFeatureIteratorFromSource<QgsOgr
QgsRectangle mFilterRect;
QgsCoordinateTransform mTransform;
QgsOgrDatasetSharedPtr mSharedDS = nullptr;
bool fetchFeatureWithId( QgsFeatureId id, QgsFeature &feature ) const;
};

View File

@ -37,6 +37,7 @@ email : sherman at mrcc.com
#include "qgsgeopackagedataitems.h"
#include "qgswkbtypes.h"
#include "qgsnetworkaccessmanager.h"
#include "qgsogrtransaction.h"
#ifdef HAVE_GUI
#include "qgssourceselectprovider.h"
@ -564,6 +565,18 @@ QString QgsOgrProvider::dataSourceUri( bool expandAuthConfig ) const
}
}
QgsTransaction *QgsOgrProvider::transaction() const
{
return static_cast<QgsTransaction *>( mTransaction );
}
void QgsOgrProvider::setTransaction( QgsTransaction *transaction )
{
QgsDebugMsg( QString( "set transaction %1" ).arg( transaction != nullptr ) );
// static_cast since layers cannot be added to a transaction of a non-matching provider
mTransaction = static_cast<QgsOgrTransaction *>( transaction );
}
QgsAbstractFeatureSource *QgsOgrProvider::featureSource() const
{
return new QgsOgrFeatureSource( this );
@ -1545,6 +1558,9 @@ bool QgsOgrProvider::addFeatures( QgsFeatureList &flist, Flags flags )
if ( returnvalue )
clearMinMaxCache();
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return returnvalue;
}
@ -1672,6 +1688,9 @@ bool QgsOgrProvider::addAttributes( const QList<QgsField> &attributes )
}
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return returnvalue;
}
@ -1706,6 +1725,10 @@ bool QgsOgrProvider::deleteAttributes( const QgsAttributeIds &attributes )
}
}
loadFields();
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return res;
}
@ -1753,13 +1776,17 @@ bool QgsOgrProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes
}
}
loadFields();
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
return result;
}
bool QgsOgrProvider::startTransaction()
{
bool inTransaction = false;
if ( mOgrLayer->TestCapability( OLCTransactions ) )
if ( mTransaction == nullptr && mOgrLayer->TestCapability( OLCTransactions ) )
{
// A transaction might already be active, so be robust on failed
// StartTransaction.
@ -2009,6 +2036,9 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_
commitTransaction();
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
if ( mOgrLayer->SyncToDisk() != OGRERR_NONE )
{
pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) );
@ -2089,6 +2119,9 @@ bool QgsOgrProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
commitTransaction();
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ) ) );
return syncToDisc();
}
@ -2191,6 +2224,9 @@ bool QgsOgrProvider::deleteFeatures( const QgsFeatureIds &id )
commitTransaction();
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
if ( !syncToDisc() )
{
returnvalue = false;
@ -2216,6 +2252,9 @@ bool QgsOgrProvider::deleteFeature( QgsFeatureId id )
return false;
}
if ( mTransaction )
mTransaction->dirtyLastSavePoint();
mShapefileMayBeCorrupted = true;
return true;
@ -2434,6 +2473,12 @@ void QgsOgrProvider::computeCapabilities()
{
ability |= CircularGeometries;
}
if ( mGDALDriverName == QLatin1String( "GPKG" ) )
{
//supports transactions
ability |= TransactionSupport;
}
}
if ( updateModeActivated )
@ -4348,6 +4393,29 @@ void QgsOgrProviderUtils::invalidateCachedDatasets( const QString &dsName )
}
}
QgsOgrDatasetSharedPtr QgsOgrProviderUtils::getAlreadyOpenedDataset( const QString &dsName )
{
QMutexLocker locker( &sGlobalMutex );
for ( auto iter = sMapSharedDS.begin(); iter != sMapSharedDS.end(); ++iter )
{
auto ident = iter.key();
if ( ident.dsName == dsName && ident.updateMode )
{
// Browse through this list, to look for the first available DatasetWithLayers*
// instance that is in update mode (hoping there's only one...)
auto &datasetList = iter.value();
for ( const auto &ds : datasetList )
{
Q_ASSERT( ds->refCount > 0 );
return QgsOgrDataset::create( ident, ds );
}
}
}
return nullptr;
}
QgsOgrLayerUniquePtr QgsOgrProviderUtils::getLayer( const QString &dsName,
int layerIndex,
QString &errCause )
@ -4987,6 +5055,45 @@ QgsOgrLayerUniquePtr QgsOgrProviderUtils::getSqlLayer( QgsOgrLayer *baseLayer,
return QgsOgrLayer::CreateForSql( ident, sql, baseLayer->ds, hSqlLayer );
}
void QgsOgrProviderUtils::releaseInternal( const DatasetIdentification &ident,
DatasetWithLayers *ds,
bool removeFromDatasetList )
{
ds->refCount --;
if ( ds->refCount == 0 )
{
Q_ASSERT( ds->setLayers.isEmpty() );
if ( removeFromDatasetList )
{
auto iter = sMapSharedDS.find( ident );
if ( iter != sMapSharedDS.end() )
{
auto &datasetList = iter.value();
int i = 0;
// Normally there should be a match, except for datasets that
// have been invalidated
Q_FOREACH ( QgsOgrProviderUtils::DatasetWithLayers *dsIter, datasetList )
{
if ( dsIter == ds )
{
datasetList.removeAt( i );
break;
}
i ++;
}
if ( datasetList.isEmpty() )
sMapSharedDS.erase( iter );
}
}
QgsOgrProviderUtils::GDALCloseWrapper( ds->hDS );
delete ds;
}
}
void QgsOgrProviderUtils::release( QgsOgrLayer *&layer )
{
if ( !layer )
@ -5004,42 +5111,71 @@ void QgsOgrProviderUtils::release( QgsOgrLayer *&layer )
GDALDatasetReleaseResultSet( layer->ds->hDS, layer->hLayer );
}
layer->ds->refCount --;
if ( layer->ds->refCount == 0 )
{
Q_ASSERT( layer->ds->setLayers.isEmpty() );
releaseInternal( layer->ident, layer->ds, !layer->isSqlLayer );
if ( !layer->isSqlLayer )
{
auto iter = sMapSharedDS.find( layer->ident );
if ( iter != sMapSharedDS.end() )
{
auto &datasetList = iter.value();
int i = 0;
// Normally there should be a match, except for datasets that
// have been invalidated
Q_FOREACH ( QgsOgrProviderUtils::DatasetWithLayers *ds, datasetList )
{
if ( ds == layer->ds )
{
datasetList.removeAt( i );
break;
}
i ++;
}
if ( datasetList.isEmpty() )
sMapSharedDS.erase( iter );
}
}
QgsOgrProviderUtils::GDALCloseWrapper( layer->ds->hDS );
delete layer->ds;
}
delete layer;
layer = nullptr;
}
void QgsOgrProviderUtils::releaseDataset( QgsOgrDataset *&ds )
{
if ( !ds )
return;
QMutexLocker locker( &sGlobalMutex );
releaseInternal( ds->mIdent, ds->mDs, true );
delete ds;
ds = nullptr;
}
QgsOgrDatasetSharedPtr QgsOgrDataset::create( const QgsOgrProviderUtils::DatasetIdentification &ident,
QgsOgrProviderUtils::DatasetWithLayers *ds )
{
QgsOgrDatasetSharedPtr dsRet = QgsOgrDatasetSharedPtr( new QgsOgrDataset(), QgsOgrProviderUtils::releaseDataset );
dsRet->mIdent = ident;
dsRet->mDs = ds;
dsRet->mDs->refCount ++;
return dsRet;
}
bool QgsOgrDataset::executeSQLNoReturn( const QString &sql )
{
QMutexLocker locker( &mutex() );
CPLErrorReset();
OGRLayerH hSqlLayer = GDALDatasetExecuteSQL(
mDs->hDS, sql.toUtf8().constData(), nullptr, nullptr );
bool ret = CPLGetLastErrorType() == CE_None;
GDALDatasetReleaseResultSet( mDs->hDS, hSqlLayer );
return ret;
}
OGRLayerH QgsOgrDataset::createSQLResultLayer( QTextCodec *encoding, const QString &layerName, int layerIndex )
{
QMutexLocker locker( &mutex() );
OGRLayerH layer;
if ( !layerName.isEmpty() )
{
layer = GDALDatasetGetLayerByName( mDs->hDS, layerName.toUtf8().constData() );
}
else
{
layer = GDALDatasetGetLayer( mDs->hDS, layerIndex );
}
if ( !layer )
return nullptr;
return QgsOgrProviderUtils::setSubsetString( layer, mDs->hDS, encoding, QString(), false, nullptr );
}
void QgsOgrDataset::releaseResultSet( OGRLayerH hSqlLayer )
{
QMutexLocker locker( &mutex() );
GDALDatasetReleaseResultSet( mDs->hDS, hSqlLayer );
}
QgsOgrLayer::QgsOgrLayer()
{
oFDefn.layer = this;
@ -6188,3 +6324,16 @@ void QgsOgrLayerReleaser::operator()( QgsOgrLayer *layer )
{
QgsOgrProviderUtils::release( layer );
}
QGISEXTERN QgsTransaction *createTransaction( const QString &connString )
{
auto ds = QgsOgrProviderUtils::getAlreadyOpenedDataset( connString );
if ( !ds )
{
QgsMessageLog::logMessage( QObject::tr( "Cannot open transaction on %1, since it is is not currently opened" ).arg( connString ),
QObject::tr( "OGR" ), Qgis::Critical );
return nullptr;
}
return new QgsOgrTransaction( connString, ds );
}

View File

@ -33,6 +33,7 @@ class QgsOgrFeatureIterator;
#include <gdal.h>
class QgsOgrLayer;
class QgsOgrTransaction;
/**
* Releases a QgsOgrLayer
@ -146,6 +147,7 @@ class QgsOgrProvider : public QgsVectorDataProvider
QString name() const override;
QString description() const override;
QgsTransaction *transaction() const override;
bool doesStrictFeatureTypeCheck() const override;
//! Returns OGR geometry type
@ -317,14 +319,27 @@ class QgsOgrProvider : public QgsVectorDataProvider
void setupProxy();
#endif
QgsOgrTransaction *mTransaction = nullptr;
void setTransaction( QgsTransaction *transaction ) override;
};
class QgsOgrDataset;
/**
* Scoped QgsOgrDataset.
*/
using QgsOgrDatasetSharedPtr = std::shared_ptr< QgsOgrDataset>;
/**
\class QgsOgrProviderUtils
\brief Utility class with static methods
*/
class QgsOgrProviderUtils
{
friend class QgsOgrDataset;
friend class QgsOgrLayer;
//! Identifies a dataset by name, updateMode and options
@ -370,6 +385,10 @@ class QgsOgrProviderUtils
static bool canUseOpenedDatasets( const QString &dsName );
static void releaseInternal( const DatasetIdentification &ident,
DatasetWithLayers *ds,
bool removeFromDatasetList );
public:
//! Inject credentials into the dsName (if any)
@ -397,6 +416,9 @@ class QgsOgrProviderUtils
//! Wrapper for GDALClose()
static void GDALCloseWrapper( GDALDatasetH mhDS );
//! Return a QgsOgrDataset wrapping an already opened GDALDataset. Typical use: by QgsOgrTransaction
static QgsOgrDatasetSharedPtr getAlreadyOpenedDataset( const QString &dsName );
//! Open a layer given by name, potentially reusing an existing GDALDatasetH if it doesn't already use that layer.
static QgsOgrLayerUniquePtr getLayer( const QString &dsName,
const QString &layerName,
@ -430,6 +452,9 @@ class QgsOgrProviderUtils
//! Release a QgsOgrLayer*
static void release( QgsOgrLayer *&layer );
//! Release a QgsOgrDataset*
static void releaseDataset( QgsOgrDataset *&ds );
//! Make sure that the existing pool of opened datasets on dsName is not accessible for new getLayer() attempts
static void invalidateCachedDatasets( const QString &dsName );
@ -448,6 +473,34 @@ class QgsOgrProviderUtils
};
/**
\class QgsOgrDataset
\brief Wrap a GDALDatasetH object in a thread-safe way
*/
class QgsOgrDataset
{
friend class QgsOgrProviderUtils;
QgsOgrProviderUtils::DatasetIdentification mIdent;
QgsOgrProviderUtils::DatasetWithLayers *mDs;
QgsOgrDataset() = default;
~QgsOgrDataset() = default;
public:
static QgsOgrDatasetSharedPtr create( const QgsOgrProviderUtils::DatasetIdentification &ident,
QgsOgrProviderUtils::DatasetWithLayers *ds );
QMutex &mutex() { return mDs->mutex; }
bool executeSQLNoReturn( const QString &sql );
OGRLayerH createSQLResultLayer( QTextCodec *encoding, const QString &layerName, int layerIndex );
void releaseResultSet( OGRLayerH hSqlLayer );
};
/**
\class QgsOgrFeatureDefn
\brief Wrap a OGRFieldDefnH object in a thread-safe way
@ -502,7 +555,7 @@ class QgsOgrLayer
QgsOgrProviderUtils::DatasetIdentification ident;
bool isSqlLayer = false;
QString layerName;
QString sql;
QString sql; // not really used. Just set at QgsOgrLayer::CreateForLayer() time
QgsOgrProviderUtils::DatasetWithLayers *ds = nullptr;
OGRLayerH hLayer = nullptr;
QgsOgrFeatureDefn oFDefn;
@ -622,7 +675,7 @@ class QgsOgrLayer
//! Wrapper of GDALDatasetReleaseResultSet( GDALDatasetExecuteSQL( ... ) )
void ExecuteSQLNoReturn( const QByteArray &sql );
//! Wrapper of GDALDatasetExecuteSQL(). Returned layer must be released with QgsOgrProviderUtils::release()
//! Wrapper of GDALDatasetExecuteSQL().
QgsOgrLayerUniquePtr ExecuteSQL( const QByteArray &sql );
};

View File

@ -0,0 +1,85 @@
/***************************************************************************
QgsOgrTransaction.cpp - Transaction support for OGR layers
-------------------
begin : June 13, 2018
copyright : (C) 2018 by Even Rouault
email : even.rouault @ spatialys.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 "qgsogrtransaction.h"
#include "qgsogrprovider.h"
#include "qgslogger.h"
#include "qgis.h"
QgsOgrTransaction::QgsOgrTransaction( const QString &connString, QgsOgrDatasetSharedPtr ds )
: QgsTransaction( connString ), mSharedDS( ds )
{
Q_ASSERT( mSharedDS );
}
bool QgsOgrTransaction::beginTransaction( QString &error, int /* statementTimeout */ )
{
return executeSql( QStringLiteral( "BEGIN" ), error );
}
bool QgsOgrTransaction::commitTransaction( QString &error )
{
if ( executeSql( QStringLiteral( "COMMIT" ), error ) )
{
return true;
}
return false;
}
bool QgsOgrTransaction::rollbackTransaction( QString &error )
{
if ( executeSql( QStringLiteral( "ROLLBACK" ), error ) )
{
return true;
}
return false;
}
bool QgsOgrTransaction::executeSql( const QString &sql, QString &errorMsg, bool isDirty, const QString &name )
{
QString err;
if ( isDirty )
{
createSavepoint( err );
}
QgsDebugMsg( QString( "Transaction sql: %1" ).arg( sql ) );
if ( !mSharedDS->executeSQLNoReturn( sql ) )
{
errorMsg = CPLGetLastErrorMsg();
QgsDebugMsg( errorMsg );
if ( isDirty )
{
rollbackToSavepoint( savePoints().last(), err );
}
return false;
}
if ( isDirty )
{
dirtyLastSavePoint();
emit dirtied( sql, name );
}
QgsDebugMsg( QString( "... ok" ) );
return true;
}

View File

@ -0,0 +1,53 @@
/***************************************************************************
qgsogrtransaction.h - Transaction support for OGR layers
-------------------
begin : June 13, 2018
copyright : (C) 2018 by Even Rouault
email : even.rouault @ spatialys.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. *
* *
***************************************************************************/
#ifndef QGSOGRTRANSACTION_H
#define QGSOGRTRANSACTION_H
#include "qgstransaction.h"
#include "qgsogrprovider.h"
class QgsOgrTransaction : public QgsTransaction
{
Q_OBJECT
public:
explicit QgsOgrTransaction( const QString &connString, QgsOgrDatasetSharedPtr ds );
/**
* 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;
QgsOgrDatasetSharedPtr sharedDS() const { return mSharedDS; }
private:
QgsOgrDatasetSharedPtr mSharedDS = nullptr;
bool beginTransaction( QString &error, int statementTimeout ) override;
bool commitTransaction( QString &error ) override;
bool rollbackTransaction( QString &error ) override;
};
#endif // QGSOGRTRANSACTION_H

View File

@ -983,6 +983,76 @@ class TestPyQgsOGRProviderGpkg(unittest.TestCase):
self.assertTrue(QgsGeometry.compare(provider_extent.asPolygon()[0], reference.asPolygon()[0], 0.00001),
provider_extent.asPolygon()[0])
def testTransaction(self):
tmpfile = os.path.join(self.basetestpath, 'testTransaction.gpkg')
ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile)
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
vl1 = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + "lyr1", 'test', u'ogr')
self.assertTrue(vl1.isValid())
vl2 = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + "lyr2", 'test', u'ogr')
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(u'{}'.format(tmpfile) + "|layername=" + "lyr1", 'test', u'ogr')
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(u'{}'.format(tmpfile) + "|layername=" + "lyr2", 'test', u'ogr')
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()