/*************************************************************************** qgsogrprovider.cpp Data provider for OGR supported formats Formerly known as qgsshapefileprovider.cpp begin : Oct 29, 2003 copyright : (C) 2003 by Gary E.Sherman email : sherman at mrcc.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 "qgsogrprovider.h" #include "moc_qgsogrprovider.cpp" ///@cond PRIVATE #include "qgscplerrorhandler_p.h" #include "qgslogger.h" #include "qgsvectorfilewriter.h" #include "qgsapplication.h" #include "qgssettings.h" #include "qgsogrconnpool.h" #include "qgsogrtransaction.h" #include "qgsogrfeatureiterator.h" #include "qgsgdalutils.h" #include "qgsfeedback.h" #include "qgscplhttpfetchoverrider.h" #include "qgsmetadatautils.h" #include "qgslocalec.h" #include "qgssymbol.h" #include "qgsembeddedsymbolrenderer.h" #include "qgsprovidersublayerdetails.h" #include "qgsvectorlayer.h" #include "qgsproviderregistry.h" #include "qgsvariantutils.h" #include "qgsjsonutils.h" #include "qgssetrequestinitiator_p.h" #include "qgsthreadingutils.h" #include #define CPL_SUPRESS_CPLUSPLUS //#spellok #include // to collect version information #include #include #include #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1) // Temporary solution for gdal < 3.2.1 without GDAL Unique support #include "qgssqliteutils.h" #include // end temporary #endif #include #include #include #include #include #include #define TEXT_PROVIDER_KEY QStringLiteral( "ogr" ) #define TEXT_PROVIDER_DESCRIPTION QStringLiteral( "OGR data provider" ) bool QgsOgrProvider::convertField( QgsField &field, const QTextCodec &encoding ) { OGRFieldType ogrType = OFTString; //default to string OGRFieldSubType ogrSubType = OFSTNone; int ogrWidth = field.length(); int ogrPrecision = field.precision(); if ( ogrPrecision > 0 ) ogrWidth += 1; switch ( field.type() ) { case QMetaType::Type::LongLong: ogrType = OFTInteger64; ogrPrecision = 0; ogrWidth = ogrWidth > 0 && ogrWidth <= 21 ? ogrWidth : 21; break; case QMetaType::Type::QString: ogrType = OFTString; if ( ogrWidth < 0 || ogrWidth > 255 ) ogrWidth = 255; break; case QMetaType::Type::Int: ogrType = OFTInteger; ogrWidth = ogrWidth > 0 && ogrWidth <= 10 ? ogrWidth : 10; ogrPrecision = 0; break; case QMetaType::Type::Bool: ogrType = OFTInteger; ogrSubType = OFSTBoolean; ogrWidth = 1; ogrPrecision = 0; break; case QMetaType::Type::Double: ogrType = OFTReal; break; case QMetaType::Type::QDate: ogrType = OFTDate; break; case QMetaType::Type::QTime: ogrType = OFTTime; break; case QMetaType::Type::QDateTime: ogrType = OFTDateTime; break; case QMetaType::Type::QStringList: { ogrType = OFTStringList; break; } case QMetaType::Type::QVariantList: if ( field.subType() == QMetaType::Type::QString ) { ogrType = OFTStringList; } else if ( field.subType() == QMetaType::Type::Int ) { ogrType = OFTIntegerList; } else if ( field.subType() == QMetaType::Type::LongLong ) { ogrType = OFTInteger64List; } else if ( field.subType() == QMetaType::Type::Double ) { ogrType = OFTRealList; } else { // other lists are not supported at this moment return false; } break; case QMetaType::Type::QVariantMap: ogrType = OFTString; ogrSubType = OFSTJSON; break; default: return false; } if ( ogrSubType != OFSTNone ) field.setTypeName( encoding.toUnicode( OGR_GetFieldSubTypeName( ogrSubType ) ) ); else field.setTypeName( encoding.toUnicode( OGR_GetFieldTypeName( ogrType ) ) ); field.setLength( ogrWidth ); field.setPrecision( ogrPrecision ); return true; } void QgsOgrProvider::repack() { if ( !mValid || mGDALDriverName != QLatin1String( "ESRI Shapefile" ) || !mOgrOrigLayer ) return; // run REPACK on shape files QByteArray sql = QByteArray( "REPACK " ) + mOgrOrigLayer->name(); // don't quote the layer name as it works with spaces in the name and won't work if the name is quoted QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 ); CPLErrorReset(); mOgrOrigLayer->ExecuteSQLNoReturn( sql ); if ( CPLGetLastErrorType() != CE_None ) { pushError( tr( "OGR[%1] error %2: %3" ).arg( CPLGetLastErrorType() ).arg( CPLGetLastErrorNo() ).arg( CPLGetLastErrorMsg() ) ); } if ( mFilePath.endsWith( QLatin1String( ".shp" ), Qt::CaseInsensitive ) || mFilePath.endsWith( QLatin1String( ".dbf" ), Qt::CaseInsensitive ) ) { QString packedDbf( mFilePath.left( mFilePath.size() - 4 ) + "_packed.dbf" ); if ( QFile::exists( packedDbf ) ) { QgsMessageLog::logMessage( tr( "Possible corruption after REPACK detected. %1 still exists. This may point to a permission or locking problem of the original DBF." ).arg( packedDbf ), tr( "OGR" ), Qgis::MessageLevel::Critical ); mOgrSqlLayer.reset(); mOgrOrigLayer.reset(); QString errCause; if ( mLayerName.isNull() ) { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, mOpenOptions, mLayerIndex, errCause, true ); } else { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, mOpenOptions, mLayerName, errCause, true ); } if ( !mOgrOrigLayer ) { QgsMessageLog::logMessage( tr( "Original layer could not be reopened." ) + " " + errCause, tr( "OGR" ), Qgis::MessageLevel::Critical ); mValid = false; } mOgrLayer = mOgrOrigLayer.get(); } } if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) ) { long long oldcount = mFeaturesCounted; recalculateFeatureCount(); if ( oldcount != mFeaturesCounted ) emit dataChanged(); } } Qgis::VectorExportResult QgsOgrProvider::createEmptyLayer( const QString &uri, const QgsFields &fields, Qgis::WkbType wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, QMap *oldToNewAttrIdxMap, QString &createdLayerUri, QString *errorMessage, const QMap *options ) { QString encoding; QString driverName = QStringLiteral( "GPKG" ); QStringList dsOptions, layerOptions; QString layerName; if ( options ) { if ( options->contains( QStringLiteral( "fileEncoding" ) ) ) encoding = options->value( QStringLiteral( "fileEncoding" ) ).toString(); if ( options->contains( QStringLiteral( "driverName" ) ) ) driverName = options->value( QStringLiteral( "driverName" ) ).toString(); if ( options->contains( QStringLiteral( "datasourceOptions" ) ) ) dsOptions << options->value( QStringLiteral( "datasourceOptions" ) ).toStringList(); if ( options->contains( QStringLiteral( "layerOptions" ) ) ) layerOptions << options->value( QStringLiteral( "layerOptions" ) ).toStringList(); if ( options->contains( QStringLiteral( "layerName" ) ) ) layerName = options->value( QStringLiteral( "layerName" ) ).toString(); } oldToNewAttrIdxMap->clear(); if ( errorMessage ) errorMessage->clear(); QgsVectorFileWriter::ActionOnExistingFile action( QgsVectorFileWriter::CreateOrOverwriteFile ); bool update = false; if ( options && options->contains( QStringLiteral( "update" ) ) ) { update = options->value( QStringLiteral( "update" ) ).toBool(); if ( update ) { // when updating an existing dataset, always use the original driver GDALDriverH hDriver = GDALIdentifyDriverEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr ); if ( hDriver ) { driverName = GDALGetDriverShortName( hDriver ); } if ( !overwrite && !layerName.isEmpty() ) { gdal::dataset_unique_ptr hDS( GDALOpenEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ) ); if ( hDS ) { if ( GDALDatasetGetLayerByName( hDS.get(), layerName.toUtf8().constData() ) ) { if ( errorMessage ) *errorMessage += QObject::tr( "Layer %2 of %1 exists and overwrite flag is false." ) .arg( uri, layerName ); return Qgis::VectorExportResult::ErrorCreatingDataSource; } } } if ( QFileInfo::exists( uri ) ) action = QgsVectorFileWriter::CreateOrOverwriteLayer; } } if ( !overwrite && !update ) { if ( QFileInfo::exists( uri ) ) { if ( errorMessage ) *errorMessage += QObject::tr( "Unable to create the datasource. %1 exists and overwrite flag is false." ) .arg( uri ); return Qgis::VectorExportResult::ErrorCreatingDataSource; } } QgsFields cleanedFields = fields; #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 6, 3) // Temporary solution. Proper fix in https://github.com/OSGeo/gdal/pull/7147 if ( driverName == QLatin1String( "OpenFileGDB" ) ) { QString fidColumn = QStringLiteral( "OBJECTID" ); for ( const QString &layerOption : layerOptions ) { if ( layerOption.startsWith( QLatin1String( "FID=" ), Qt::CaseInsensitive ) ) { fidColumn = layerOption.mid( static_cast( strlen( "FID=" ) ) ); break; } } const int objectIdIndex = cleanedFields.lookupField( fidColumn ); if ( objectIdIndex >= 0 ) { cleanedFields.remove( objectIdIndex ); } } #endif QString newLayerName( layerName ); QgsVectorFileWriter::SaveVectorOptions saveOptions; saveOptions.layerName = layerName; saveOptions.fileEncoding = encoding; saveOptions.driverName = driverName; saveOptions.datasourceOptions = dsOptions; saveOptions.layerOptions = layerOptions; saveOptions.actionOnExistingFile = action; saveOptions.symbologyExport = Qgis::FeatureSymbologyExport::NoSymbology; std::unique_ptr< QgsVectorFileWriter > writer( QgsVectorFileWriter::create( uri, cleanedFields, wkbType, srs, QgsCoordinateTransformContext(), saveOptions, QgsFeatureSink::SinkFlags(), nullptr, &newLayerName ) ); layerName = newLayerName; QVariantMap uriParts = QgsOgrProviderMetadata().decodeUri( uri ); uriParts.insert( QStringLiteral( "layerName" ), newLayerName ); createdLayerUri = QgsOgrProviderMetadata().encodeUri( uriParts ); QgsVectorFileWriter::WriterError error = writer->hasError(); if ( error ) { if ( errorMessage ) *errorMessage += writer->errorMessage(); return static_cast( error ); } QMap attrIdxMap = writer->sourceFieldIndexToWriterFieldIndex(); writer.reset(); { bool firstFieldIsFid = false; bool fidColumnIsField = false; if ( !layerName.isEmpty() ) { gdal::dataset_unique_ptr hDS( GDALOpenEx( uri.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ) ); if ( hDS ) { OGRLayerH hLayer = GDALDatasetGetLayerByName( hDS.get(), layerName.toUtf8().constData() ); if ( hLayer ) { // Expose the OGR FID if it comes from a "real" column (typically GPKG) // and make sure that this FID column is not exposed as a regular OGR field (shouldn't happen normally) const QString ogrFidColumnName { OGR_L_GetFIDColumn( hLayer ) }; firstFieldIsFid = !( EQUAL( OGR_L_GetFIDColumn( hLayer ), "" ) ) && OGR_FD_GetFieldIndex( OGR_L_GetLayerDefn( hLayer ), ogrFidColumnName.toUtf8() ) < 0 && cleanedFields.indexFromName( ogrFidColumnName.toUtf8() ) < 0; // At this point we must check if there is a real FID field in the the fields argument, // because in that case we don't want to shift all fields (see issue GH #34333) // Check for unique values should be performed in client code. for ( const auto &f : std::as_const( fields ) ) { if ( f.name().compare( ogrFidColumnName, Qt::CaseSensitivity::CaseInsensitive ) == 0 ) { fidColumnIsField = true; break; } } } } } const bool shiftColumnsByOne { firstFieldIsFid &&( ! fidColumnIsField ) }; for ( QMap::const_iterator attrIt = attrIdxMap.constBegin(); attrIt != attrIdxMap.constEnd(); ++attrIt ) { oldToNewAttrIdxMap->insert( attrIt.key(), *attrIt + ( shiftColumnsByOne ? 1 : 0 ) ); } } QgsOgrProviderUtils::invalidateCachedLastModifiedDate( uri ); return Qgis::VectorExportResult::Success; } QgsOgrProvider::QgsOgrProvider( QString const &uri, const ProviderOptions &options, Qgis::DataProviderReadFlags flags ) : QgsVectorDataProvider( uri, options, flags ) { QgsApplication::registerOgrDrivers(); QgsSettings settings; // we always disable GDAL side shapefile encoding handling, and do it on the QGIS side. // why? it's not the ideal choice, but... // - if we DON'T disable GDAL side encoding support, then there's NO way to change the encoding used when reading // shapefiles. And unfortunately the embedded encoding (which is read by GDAL) is sometimes wrong, so we need // to expose support for users to be able to change and correct this // - we can't change this setting on-the-fly. If we don't set it upfront, we can't reverse this decision later when // a user does want/need to manually specify the encoding CPLSetConfigOption( "SHAPE_ENCODING", "" ); #ifndef QT_NO_NETWORKPROXY QgsGdalUtils::setupProxy(); #endif // make connection to the data source QgsDebugMsgLevel( "Data source uri is [" + uri + ']', 2 ); mFilePath = QgsOgrProviderUtils::analyzeURI( uri, mIsSubLayer, mLayerIndex, mLayerName, mSubsetString, mOgrGeometryTypeFilter, mOpenOptions, mCredentialOptions ); const QVariantMap parts = QgsOgrProviderMetadata().decodeUri( uri ); if ( parts.contains( QStringLiteral( "uniqueGeometryType" ) ) ) { mUniqueGeometryType = parts.value( QStringLiteral( "uniqueGeometryType" ) ).toString() == QLatin1String( "yes" ); } const QString vsiPrefix = parts.value( QStringLiteral( "vsiPrefix" ) ).toString(); if ( !mCredentialOptions.isEmpty() && !vsiPrefix.isEmpty() ) { const thread_local QRegularExpression bucketRx( QStringLiteral( "^(.*)/" ) ); const QRegularExpressionMatch bucketMatch = bucketRx.match( parts.value( QStringLiteral( "path" ) ).toString() ); if ( bucketMatch.hasMatch() ) { QgsGdalUtils::applyVsiCredentialOptions( vsiPrefix, bucketMatch.captured( 1 ), mCredentialOptions ); } } // to be called only after mFilePath has been set invalidateNetworkCache(); if ( uri.contains( QLatin1String( "authcfg" ) ) ) { const thread_local QRegularExpression authcfgRe( QStringLiteral( " authcfg='([^']+)'" ) ); QRegularExpressionMatch match; if ( uri.contains( authcfgRe, &match ) ) { mAuthCfg = match.captured( 1 ); // momentarily re-add authcfg since it was stripped off in analyzeURI mFilePath = QgsOgrProviderUtils::expandAuthConfig( QStringLiteral( "%1 authcfg='%2'" ).arg( mFilePath, mAuthCfg ) ); } } QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( mReadFlags & Qgis::DataProviderReadFlag::ForceReadOnly ) { open( OpenModeForceReadOnly ); } else { open( OpenModeInitial ); } QList nativeTypes; if ( mOgrOrigLayer ) { nativeTypes = QgsOgrUtils::nativeFieldTypesForDriver( mOgrOrigLayer->driver() ); } setNativeTypes( nativeTypes ); if ( mOgrOrigLayer ) { bool is3D = OGR_GT_HasZ( mOGRGeomType ); /* We will not analyse the first geometry to compute if we have 3D or 2D data. * We will believe in the metadata, even if the metadata is corrupted or misdefined. * Below, here is an example of how retrieve the 3D/2D state according to the first geometry: */ // if ( !is3D ) // { // gdal::ogr_feature_unique_ptr f; // mOgrOrigLayer->ResetReading(); // f.reset( mOgrOrigLayer->GetNextFeature() ); // if ( f ) // { // OGRGeometryH g = OGR_F_GetGeometryRef( f.get() ); // if ( g && !OGR_G_IsEmpty( g ) ) // { // is3D = OGR_G_Is3D( g ); // } // } // } elevationProperties()->setContainsElevationData( is3D ); mOgrOrigLayer->ResetReading(); // release } // layer metadata loadMetadata(); QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); } QgsOgrProvider::~QgsOgrProvider() { QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); // We must also make sure to flush unusef cached connections so that // the file can be removed (#15137) QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); // Do that as last step for final cleanup that might be prevented by // still opened datasets. close(); } QString QgsOgrProvider::dataSourceUri( bool expandAuthConfig ) const { if ( expandAuthConfig && QgsDataProvider::dataSourceUri( ).contains( QLatin1String( "authcfg" ) ) ) { return QgsOgrProviderUtils::expandAuthConfig( QgsDataProvider::dataSourceUri( ) ); } else { return QgsDataProvider::dataSourceUri( ); } } QgsTransaction *QgsOgrProvider::transaction() const { return static_cast( mTransaction ); } void QgsOgrProvider::setTransaction( QgsTransaction *transaction ) { QgsDebugMsgLevel( QStringLiteral( "set transaction %1" ).arg( transaction != nullptr ), 2 ); // static_cast since layers cannot be added to a transaction of a non-matching provider mTransaction = static_cast( transaction ); if ( transaction ) { connect( mTransaction, &QgsTransaction::afterRollback, this, &QgsOgrProvider::afterRollback ); connect( mTransaction, &QgsTransaction::afterRollbackToSavepoint, this, &QgsOgrProvider::afterRollbackToSavepoint ); } } QgsAbstractFeatureSource *QgsOgrProvider::featureSource() const { return new QgsOgrFeatureSource( this ); } bool QgsOgrProvider::setSubsetString( const QString &theSQL, bool updateFeatureCount ) { return _setSubsetString( theSQL, updateFeatureCount, true ); } QString QgsOgrProvider::subsetString() const { return mSubsetString; } bool QgsOgrProvider::supportsSubsetString() const { return true; } QString QgsOgrProvider::subsetStringDialect() const { #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0) if ( mOgrLayer ) { if ( const char *pszSqlDialects = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_SUPPORTED_SQL_DIALECTS, nullptr ) ) { const QStringList dialects = QString( pszSqlDialects ).split( ' ' ); // first dialect is default, which is what QGIS uses const QString defaultDialect = !dialects.isEmpty() ? dialects.at( 0 ) : QString(); if ( defaultDialect == QLatin1String( "NATIVE" ) ) { return tr( "%1 query" ).arg( GDALGetDriverLongName( mOgrLayer->driver() ) ); } else if ( defaultDialect == QLatin1String( "OGRSQL" ) ) { return tr( "OGR SQL query" ); } else if ( defaultDialect == QLatin1String( "SQLITE" ) ) { return tr( "SQLite query" ); } return defaultDialect; } } #endif return QString(); } QString QgsOgrProvider::subsetStringHelpUrl() const { #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0) if ( mOgrLayer ) { if ( const char *pszSqlDialects = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_SUPPORTED_SQL_DIALECTS, nullptr ) ) { const QStringList dialects = QString( pszSqlDialects ).split( ' ' ); // first dialect is default, which is what QGIS uses const QString defaultDialect = !dialects.isEmpty() ? dialects.at( 0 ) : QString(); if ( defaultDialect == QLatin1String( "NATIVE" ) ) { return QgsGdalUtils::gdalDocumentationUrlForDriver( mOgrLayer->driver() ); } else if ( defaultDialect == QLatin1String( "OGRSQL" ) ) { return QStringLiteral( "https://gdal.org/user/ogr_sql_dialect.html" ); } else if ( defaultDialect == QLatin1String( "SQLITE" ) ) { return QStringLiteral( "https://gdal.org/user/sql_sqlite_dialect.html" ); } } } #endif return QString(); } uint QgsOgrProvider::subLayerCount() const { uint count = layerCount(); if ( count == 0 ) return 0; QString errCause; QgsOgrLayerUniquePtr layerStyles = QgsOgrProviderUtils::getLayer( mFilePath, QStringLiteral( "layer_styles" ), errCause ); if ( layerStyles ) { count--; } return count; } QStringList subLayerDetailsToStringList( const QList< QgsProviderSublayerDetails > &layers ) { QStringList res; res.reserve( layers.size() ); for ( const QgsProviderSublayerDetails &layer : layers ) { const OGRwkbGeometryType ogrGeomType = QgsOgrProviderUtils::ogrTypeFromQgisType( layer.wkbType() ); const QStringList parts { QString::number( layer.layerNumber() ), layer.name(), QString::number( layer.featureCount() ), QgsOgrProviderUtils::ogrWkbGeometryTypeName( ogrGeomType ), layer.geometryColumnName(), layer.description() }; res << parts.join( QgsDataProvider::sublayerSeparator() ); } return res; } QStringList QgsOgrProvider::subLayers() const { const bool withFeatureCount = ( mReadFlags & Qgis::DataProviderReadFlag::SkipFeatureCount ) == 0; Qgis::SublayerQueryFlags flags = withFeatureCount ? ( Qgis::SublayerQueryFlag::CountFeatures | Qgis::SublayerQueryFlag::ResolveGeometryType ) : Qgis::SublayerQueryFlag::ResolveGeometryType; if ( mIsSubLayer ) flags |= Qgis::SublayerQueryFlag::IncludeSystemTables; return subLayerDetailsToStringList( _subLayers( flags ) ); } QgsLayerMetadata QgsOgrProvider::layerMetadata() const { return mLayerMetadata; } QList QgsOgrProvider::_subLayers( Qgis::SublayerQueryFlags flags ) const { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !mValid ) { return {}; } if ( !mSubLayerList.isEmpty() ) return mSubLayerList; const size_t totalLayerCount = layerCount(); if ( mOgrLayer && ( mIsSubLayer || totalLayerCount == 1 ) ) { mSubLayerList << QgsOgrProviderUtils::querySubLayerList( mLayerIndex, mOgrLayer, nullptr, mGDALDriverName, flags, dataSourceUri(), totalLayerCount == 1 ); } else { // In case there is no free opened dataset in the cache, keep the first // layer alive while we iterate over the other layers, so that we can // reuse the same dataset. Can help in a particular with a FileGDB with // the FileGDB driver QgsOgrLayerUniquePtr firstLayer; for ( size_t i = 0; i < totalLayerCount ; i++ ) { QString errCause; QgsOgrLayerUniquePtr layer = QgsOgrProviderUtils::getLayer( mOgrOrigLayer->datasetName(), mOgrOrigLayer->updateMode(), mOgrOrigLayer->options(), i, errCause, // do not check timestamp beyond the first // layer !firstLayer ); if ( !layer ) continue; mSubLayerList << QgsOgrProviderUtils::querySubLayerList( i, layer.get(), nullptr, mGDALDriverName, flags, dataSourceUri(), totalLayerCount == 1 ); if ( !firstLayer ) { firstLayer = std::move( layer ); } } } return mSubLayerList; } void QgsOgrProvider::afterRollbackToSavepoint( const QString &savepointName ) { Q_UNUSED( savepointName ); mFieldsRequireReload = true; } void QgsOgrProvider::afterRollback() { mFieldsRequireReload = true; } void QgsOgrProvider::setEncoding( const QString &e ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); QgsSettings settings; // if the layer has the OLCStringsAsUTF8 capability, we CANNOT override the // encoding on the QGIS side! if ( mOgrLayer && !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) ) { QgsVectorDataProvider::setEncoding( e ); } else { QgsVectorDataProvider::setEncoding( QStringLiteral( "UTF-8" ) ); } loadFields(); } // This is reused by dataItem OGRwkbGeometryType QgsOgrProvider::getOgrGeomType( const QString &driverName, OGRLayerH ogrLayer ) { OGRFeatureDefnH fdef = OGR_L_GetLayerDefn( ogrLayer ); OGRwkbGeometryType geomType = wkbUnknown; if ( fdef ) { geomType = OGR_FD_GetGeomType( fdef ); // Handle wkbUnknown and its Z/M variants. QGIS has no unknown Z/M variants, // so just use flat wkbUnknown if ( wkbFlatten( geomType ) == wkbUnknown ) geomType = wkbUnknown; // Some ogr drivers (e.g. GML) are not able to determine the geometry type of a layer like this. // In such cases, we use virtual sublayers for each geometry if the layer contains // multiple geometries (see subLayers) otherwise we guess geometry type from the first // feature that has a geometry (limit us to a few features, not the whole layer) // // For ESRI formats with a GeometryCollection25D type we also query features for the geometry type, // as they may be ESRI MultiPatch files which we want to report as MultiPolygon25D types if ( geomType == wkbUnknown || ( geomType == wkbGeometryCollection25D && ( driverName == QLatin1String( "ESRI Shapefile" ) || driverName == QLatin1String( "OpenFileGDB" ) || driverName == QLatin1String( "FileGDB" ) ) ) ) { geomType = wkbNone; OGR_L_ResetReading( ogrLayer ); for ( int i = 0; i < 10; i++ ) { gdal::ogr_feature_unique_ptr nextFeature( OGR_L_GetNextFeature( ogrLayer ) ); if ( !nextFeature ) break; geomType = QgsOgrProviderUtils::resolveGeometryTypeForFeature( nextFeature.get(), driverName ); if ( geomType != wkbNone ) break; } OGR_L_ResetReading( ogrLayer ); } } return geomType; } void QgsOgrProvider::loadFields() { QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); //the attribute fields need to be read again when the encoding changes mAttributeFields.clear(); mDefaultValues.clear(); mPrimaryKeyAttrs.clear(); if ( !mOgrLayer ) return; if ( mOgrGeometryTypeFilter != wkbUnknown ) { mOGRGeomType = mOgrGeometryTypeFilter; } else { QRecursiveMutex *mutex = nullptr; OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( mutex ); QMutexLocker locker( mutex ); mOGRGeomType = getOgrGeomType( mGDALDriverName, ogrLayer ); } QgsOgrFeatureDefn &fdef = mOgrLayer->GetLayerDefn(); // Expose the OGR FID if it comes from a "real" column (typically GPKG) // and make sure that this FID column is not exposed as a regular OGR field (shouldn't happen normally) QByteArray fidColumn( mOgrLayer->GetFIDColumn() ); mFirstFieldIsFid = !fidColumn.isEmpty() && fdef.GetFieldIndex( fidColumn ) < 0; #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1) // This is a temporary solution until GDAL Unique support is available QSet uniqueFieldNames; if ( mGDALDriverName == QLatin1String( "GPKG" ) ) { sqlite3_database_unique_ptr dsPtr; if ( dsPtr.open_v2( mFilePath, SQLITE_OPEN_READONLY, nullptr ) == SQLITE_OK ) { QString errMsg; uniqueFieldNames = QgsSqliteUtils::uniqueFields( dsPtr.get(), mOgrLayer->name(), errMsg ); if ( ! errMsg.isEmpty() ) { QgsMessageLog::logMessage( tr( "GPKG error searching for unique constraints on fields for table %1. (%2)" ).arg( QString( mOgrLayer->name() ), errMsg ), tr( "OGR" ) ); } } } #endif int createdFields = 0; if ( mFirstFieldIsFid ) { QgsField fidField( fidColumn, QMetaType::Type::LongLong, QStringLiteral( "Integer64" ) ); // Set constraints for feature id QgsFieldConstraints constraints = fidField.constraints(); constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); fidField.setConstraints( constraints ); mAttributeFields.append( fidField ); mDefaultValues.insert( 0, tr( "Autogenerate" ) ); createdFields++; mPrimaryKeyAttrs << 0; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0) // needed for field domain retrieval on GDAL 3.3+ QRecursiveMutex *datasetMutex = nullptr; GDALDatasetH ds = mOgrLayer->getDatasetHandleAndMutex( datasetMutex ); QMutexLocker locker( datasetMutex ); #endif for ( int fieldIndex = 0; fieldIndex < fdef.GetFieldCount(); ++fieldIndex ) { OGRFieldDefnH fldDef = fdef.GetFieldDefn( fieldIndex ); const OGRFieldType ogrType = OGR_Fld_GetType( fldDef ); const OGRFieldSubType ogrSubType = OGR_Fld_GetSubType( fldDef ); QMetaType::Type varType = QMetaType::Type::UnknownType; QMetaType::Type varSubType = QMetaType::Type::UnknownType; QgsOgrUtils::ogrFieldTypeToQVariantType( ogrType, ogrSubType, varType, varSubType ); // Handle special case for OGRFieldType::OFSTJSON which is not necessarily a map. // If subtype is JSON try to load a feature and check if it's // really an object (rather than something else like an array) // fallback to string. if ( ( ogrType == OFTString || ogrType == OFTWideString ) && ogrSubType == OFSTJSON ) { QRecursiveMutex *layerMutex = nullptr; OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( layerMutex ); QMutexLocker layerLocker( layerMutex ); gdal::ogr_feature_unique_ptr f( OGR_L_GetNextFeature( ogrLayer ) ); if ( f ) { const char *json = OGR_F_GetFieldAsString( f.get(), fieldIndex ); if ( json && json[0] != '\0' ) { try { const nlohmann::json json_element = json::parse( json ); // Check if it's an homogeneous array of numbers or strings if ( json_element.is_array() ) { // Check whether the values are all of the same type bool allNumbers = true; bool allIntegers = true; bool allStrings = true; for ( auto &value : json_element ) { if ( allStrings && !value.is_string() ) { allStrings = false; } if ( allNumbers && !value.is_number() ) { allNumbers = false; } if ( allIntegers && !value.is_number_integer() ) { allIntegers = false; } } if ( allNumbers ) { if ( allIntegers ) { varType = QMetaType::Type::QVariantList; varSubType = QMetaType::Type::LongLong; } else { varType = QMetaType::Type::QVariantList; varSubType = QMetaType::Type::Double; } } else if ( allStrings ) { varType = QMetaType::Type::QStringList; varSubType = QMetaType::Type::UnknownType; } else { QgsDebugMsgLevel( QStringLiteral( "JSON array contains mixed types, falling back to string" ), 2 ); varType = QMetaType::Type::QString; varSubType = QMetaType::Type::UnknownType; } } else if ( ! json_element.is_object() ) { QgsDebugMsgLevel( QStringLiteral( "JSON is neither an array nor an object, falling back to string" ), 2 ); varType = QMetaType::Type::QString; varSubType = QMetaType::Type::UnknownType; } else if ( json_element.is_number() ) { if ( json_element.is_number_float() ) { varType = QMetaType::Type::Double; varSubType = QMetaType::Type::UnknownType; } else { varType = QMetaType::Type::LongLong; varSubType = QMetaType::Type::UnknownType; } } else if ( json_element.is_string() ) { varType = QMetaType::Type::QString; varSubType = QMetaType::Type::UnknownType; } else { QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 ); } } catch ( const json::parse_error & ) { QgsDebugMsgLevel( QStringLiteral( "JSON is not valid, falling back to string" ), 2 ); varType = QMetaType::Type::QString; varSubType = QMetaType::Type::UnknownType; } } OGR_L_ResetReading( ogrLayer ); } } //TODO: fix this hack #ifdef ANDROID QString name = OGR_Fld_GetNameRef( fldDef ); #else QString name = textEncoding()->toUnicode( OGR_Fld_GetNameRef( fldDef ) ); #endif if ( mAttributeFields.indexFromName( name ) != -1 ) { QString tmpname = name + "_%1"; int fix = 0; while ( mAttributeFields.indexFromName( name ) != -1 ) { name = tmpname.arg( ++fix ); } } int width = OGR_Fld_GetWidth( fldDef ); int prec = OGR_Fld_GetPrecision( fldDef ); if ( prec > 0 ) width -= 1; QString typeName = OGR_GetFieldTypeName( ogrType ); if ( ogrSubType != OFSTNone ) typeName = OGR_GetFieldSubTypeName( ogrSubType ); QgsField newField = QgsField( name, varType, #ifdef ANDROID typeName, #else textEncoding()->toUnicode( typeName.toStdString().c_str() ), #endif width, prec, QString(), varSubType ); const QString alias = textEncoding()->toUnicode( OGR_Fld_GetAlternativeNameRef( fldDef ) ); if ( !alias.isEmpty() ) { newField.setAlias( alias ); } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) { const QString comment = textEncoding()->toUnicode( OGR_Fld_GetComment( fldDef ) ); if ( !comment.isEmpty() ) { newField.setComment( comment ); } } #endif // check if field is nullable bool nullable = OGR_Fld_IsNullable( fldDef ); if ( !nullable ) { QgsFieldConstraints constraints; constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); newField.setConstraints( constraints ); } #if GDAL_VERSION_NUM < GDAL_COMPUTE_VERSION(3, 2, 1) if ( uniqueFieldNames.contains( OGR_Fld_GetNameRef( fldDef ) ) ) #else if ( OGR_Fld_IsUnique( fldDef ) ) #endif { QgsFieldConstraints constraints = newField.constraints(); constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); newField.setConstraints( constraints ); } // check if field has default value QString defaultValue = textEncoding()->toUnicode( OGR_Fld_GetDefault( fldDef ) ); if ( defaultValue == QLatin1String( "FILEGEODATABASE_SHAPE_LENGTH" ) || defaultValue == QLatin1String( "FILEGEODATABASE_SHAPE_AREA" ) ) { // FileGeodatabase may have special fields with autocomputed geometry // length and area. Their content is generated by the driver. The // user must not fill it (if they do, it will be overridden by the // driver) newField.setReadOnly( true ); } else if ( !defaultValue.isEmpty() && !OGR_Fld_IsDefaultDriverSpecific( fldDef ) ) { if ( defaultValue.startsWith( '\'' ) ) { defaultValue = defaultValue.remove( 0, 1 ); defaultValue.chop( 1 ); defaultValue.replace( QLatin1String( "''" ), QLatin1String( "'" ) ); } mDefaultValues.insert( createdFields, defaultValue ); } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,3,0) if ( const char *domainName = OGR_Fld_GetDomainName( fldDef ) ) { QgsFieldConstraints constraints = newField.constraints(); constraints.setDomainName( domainName ); newField.setConstraints( constraints ); // dataset retains ownership of domain! if ( OGRFieldDomainH domain = GDALDatasetGetFieldDomain( ds, domainName ) ) { switch ( OGR_FldDomain_GetSplitPolicy( domain ) ) { case OFDSP_DEFAULT_VALUE: newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::DefaultValue ); break; case OFDSP_DUPLICATE: newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::Duplicate ); break; case OFDSP_GEOMETRY_RATIO: newField.setSplitPolicy( Qgis::FieldDomainSplitPolicy::GeometryRatio ); break; } switch ( OGR_FldDomain_GetDomainType( domain ) ) { case OFDT_CODED: { QVariantList valueConfig; const OGRCodedValue *codedValue = OGR_CodedFldDomain_GetEnumeration( domain ); while ( codedValue && codedValue->pszCode ) { const QString code( codedValue->pszCode ); // if pszValue is null then it indicates we are working with a set of acceptable values which aren't // coded. In this case we copy the code as the value so that QGIS exposes the domain as a choice of // the valid code values. const QString value( codedValue->pszValue ? codedValue->pszValue : codedValue->pszCode ); QVariantMap config; config[ value ] = code; valueConfig.append( config ); codedValue++; } QVariantMap editorConfig; editorConfig.insert( QStringLiteral( "map" ), valueConfig ); newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "ValueMap" ), editorConfig ) ); break; } case OFDT_RANGE: if ( newField.isNumeric() ) { // QGIS doesn't support the inclusive option yet! bool isInclusive = false; QVariantMap editorConfig; editorConfig.insert( QStringLiteral( "Step" ), 1 ); editorConfig.insert( QStringLiteral( "Style" ), QStringLiteral( "SpinBox" ) ); editorConfig.insert( QStringLiteral( "AllowNull" ), nullable ); editorConfig.insert( QStringLiteral( "Precision" ), newField.precision() ); OGRFieldType domainFieldType = OGR_FldDomain_GetFieldType( domain ); bool hasMinOrMax = false; if ( const OGRField *min = OGR_RangeFldDomain_GetMin( domain, &isInclusive ) ) { const QVariant minValue = QgsOgrUtils::OGRFieldtoVariant( min, domainFieldType ); if ( minValue.isValid() ) { editorConfig.insert( QStringLiteral( "Min" ), minValue ); hasMinOrMax = true; } } if ( const OGRField *max = OGR_RangeFldDomain_GetMax( domain, &isInclusive ) ) { const QVariant maxValue = QgsOgrUtils::OGRFieldtoVariant( max, domainFieldType ); if ( maxValue.isValid() ) { editorConfig.insert( QStringLiteral( "Max" ), maxValue ); hasMinOrMax = true; } } if ( hasMinOrMax ) newField.setEditorWidgetSetup( QgsEditorWidgetSetup( QStringLiteral( "Range" ), editorConfig ) ); } // GDAL also supports range domains for fields types like date/datetimes, but the QGIS corresponding field // config doesn't support this yet! break; case OFDT_GLOB: // not supported by QGIS yet break; } } } #endif mAttributeFields.append( newField ); createdFields++; } mFieldsRequireReload = false; } void QgsOgrProvider::loadMetadata() { // Set default, may be overridden by stored metadata mLayerMetadata.setCrs( crs() ); if ( mOgrOrigLayer ) { QRecursiveMutex *mutex = nullptr; OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex ); QMutexLocker locker( mutex ); const QString identifier = GDALGetMetadataItem( layer, "IDENTIFIER", "" ); if ( !identifier.isEmpty() ) mLayerMetadata.setTitle( identifier ); // see geopackage specs -- "'identifier' is analogous to 'title'" const QString abstract = GDALGetMetadataItem( layer, "DESCRIPTION", "" ); if ( !abstract.isEmpty() ) mLayerMetadata.setAbstract( abstract ); if ( mGDALDriverName == QLatin1String( "GPKG" ) ) { // first check if metadata tables/extension exists QString sql = QStringLiteral( "SELECT name FROM sqlite_master WHERE name='gpkg_metadata' AND type='table'" ); bool metadataTableExists = false; if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) { metadataTableExists = true; } } if ( metadataTableExists ) { // read Geopackage layer metadata - scan gpkg_metadata table for QGIS metadata sql = QStringLiteral( "SELECT metadata from gpkg_metadata LEFT JOIN gpkg_metadata_reference ON " "(gpkg_metadata_reference.table_name = %1 AND gpkg_metadata.id = gpkg_metadata_reference.md_file_id) " "WHERE md_standard_uri = %2 and reference_scope = %3" ).arg( QgsSqliteUtils::quotedString( mOgrOrigLayer->name() ), QgsSqliteUtils::quotedString( QStringLiteral( "http://mrcc.com/qgis.dtd" ) ), QgsSqliteUtils::quotedString( QStringLiteral( "table" ) ) ); if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql.toUtf8().constData() ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) { bool ok = false; QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), QgsField( QString(), QMetaType::Type::QString ), 0, nullptr, &ok ); if ( ok ) { QDomDocument doc; doc.setContent( res.toString() ); mLayerMetadata.readMetadataXml( doc.documentElement() ); } } } } } else if ( mGDALDriverName == QLatin1String( "FileGDB" ) || mGDALDriverName == QLatin1String( "OpenFileGDB" ) #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,0) || mGDALDriverName == QLatin1String( "PGeo" ) // supported on GDAL 3.4+ only #endif ) { // read ESRI FileGeodatabase/Personal Geodatabase layer metadata // important -- this ONLY works if the layer name is NOT quoted!! QByteArray sql = "GetLayerMetadata " + mOgrOrigLayer->name(); if ( QgsOgrLayerUniquePtr l = mOgrOrigLayer->ExecuteSQL( sql ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) { bool ok = false; QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), QgsField( QString(), QMetaType::Type::QString ), 0, textEncoding(), &ok ); if ( ok ) { QDomDocument metadataDoc; metadataDoc.setContent( res.toString() ); mLayerMetadata = QgsMetadataUtils::convertFromEsri( metadataDoc ); } } } } else if ( ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) ) { // look for .shp.xml sidecar file const QString sidecarPath = mFilePath + ".xml"; if ( QFileInfo::exists( sidecarPath ) ) { QFile file( sidecarPath ); if ( file.open( QFile::ReadOnly ) ) { QDomDocument doc; int line, column; QString errorMessage; if ( doc.setContent( &file, &errorMessage, &line, &column ) ) { mLayerMetadata = QgsMetadataUtils::convertFromEsri( doc ); } else { QgsDebugError( QStringLiteral( "Error reading %1: %2 at line %3 column %4" ).arg( sidecarPath, errorMessage ).arg( line ).arg( column ) ); } file.close(); } else { QgsDebugError( QStringLiteral( "Error reading %1 - could not open file for read" ).arg( sidecarPath ) ); } } } } mLayerMetadata.setType( QStringLiteral( "dataset" ) ); } QString QgsOgrProvider::storageType() const { // Delegate to the driver loaded in by OGR return mGDALDriverName; } void QgsOgrProvider::setRelevantFields( bool fetchGeometry, const QgsAttributeList &fetchAttributes ) const { QRecursiveMutex *mutex = nullptr; OGRLayerH ogrLayer = mOgrLayer->getHandleAndMutex( mutex ); QMutexLocker locker( mutex ); QgsOgrProviderUtils::setRelevantFields( ogrLayer, fields().count(), fetchGeometry, fetchAttributes, mFirstFieldIsFid, QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ); } QgsFeatureIterator QgsOgrProvider::getFeatures( const QgsFeatureRequest &request ) const { return QgsFeatureIterator( new QgsOgrFeatureIterator( static_cast( featureSource() ), true, request, mTransaction ) ); } unsigned char *QgsOgrProvider::getGeometryPointer( OGRFeatureH fet ) { OGRGeometryH geom = OGR_F_GetGeometryRef( fet ); unsigned char *gPtr = nullptr; if ( !geom ) return nullptr; // get the wkb representation gPtr = new unsigned char[OGR_G_WkbSize( geom )]; OGR_G_ExportToWkb( geom, ( OGRwkbByteOrder ) QgsApplication::endian(), gPtr ); return gPtr; } QgsRectangle QgsOgrProvider::extent() const { QgsDebugMsgLevel( QStringLiteral( "QgsOgrProvider::extent()" ), 3 ); if ( !mExtent2D && !mExtent3D ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); mExtent2D.reset( new OGREnvelope() ); // get the extent_ (envelope) of the layer QgsDebugMsgLevel( QStringLiteral( "Starting computing extent. subset: '%1'" ).arg( mSubsetString ), 3 ); if ( mForceRecomputeExtent && mValid && mWriteAccess && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) && mOgrOrigLayer ) { // works with unquoted layerName QByteArray sql = QByteArray( "RECOMPUTE EXTENT ON " ) + mOgrOrigLayer->name(); QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 ); mOgrOrigLayer->ExecuteSQLNoReturn( sql ); } mExtent2D->MinX = std::numeric_limits::max(); mExtent2D->MinY = std::numeric_limits::max(); mExtent2D->MaxX = -std::numeric_limits::max(); mExtent2D->MaxY = -std::numeric_limits::max(); // TODO: This can be expensive, do we really need it! if ( mOgrLayer == mOgrOrigLayer.get() && mSubsetString.isEmpty() ) { if ( ( mGDALDriverName == QLatin1String( "OAPIF" ) || mGDALDriverName == QLatin1String( "WFS3" ) ) && !mOgrLayer->TestCapability( OLCFastGetExtent ) ) { // When the extent is not in the metadata, retrieving it would be // super slow mExtent2D->MinX = -180; mExtent2D->MinY = -90; mExtent2D->MaxX = 180; mExtent2D->MaxY = 90; } else { QgsDebugMsgLevel( QStringLiteral( "Will call mOgrLayer->GetExtent" ), 3 ); OGRErr err = mOgrLayer->GetExtent( mExtent2D.get(), true ); if ( err != OGRERR_NONE ) { QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute extent2D (ogr error: %1)" ).arg( err ), 1 ); mExtent2D.reset(); return QgsRectangle(); } } } else { QgsDebugMsgLevel( QStringLiteral( "will apply slow default 2D extent computing" ), 3 ); OGREnvelope3D sExtent3D; OGRErr err = mOgrLayer->computeExtent3DSlowly( &sExtent3D ); if ( err == OGRERR_NONE ) { mExtent2D->MinX = sExtent3D.MinX; mExtent2D->MinY = sExtent3D.MinY; mExtent2D->MaxX = sExtent3D.MaxX; mExtent2D->MaxY = sExtent3D.MaxY; } else { QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute slow extent2D (ogr error: %1)" ).arg( err ), 1 ); mExtent2D.reset(); return QgsRectangle(); } } } if ( mExtent2D ) { mExtentRect = QgsBox3D( mExtent2D->MinX, mExtent2D->MinY, std::numeric_limits::quiet_NaN(), mExtent2D->MaxX, mExtent2D->MaxY, std::numeric_limits::quiet_NaN() ); QgsDebugMsgLevel( QStringLiteral( "Finished get extent from 2D: (%1, %2, : %3, %4)" ) .arg( mExtentRect.xMinimum() ).arg( mExtentRect.yMinimum() ) .arg( mExtentRect.xMaximum() ).arg( mExtentRect.yMaximum() ), 3 ); } else { mExtentRect = QgsBox3D( mExtent3D->MinX, mExtent3D->MinY, mExtent3D->MinZ, mExtent3D->MaxX, mExtent3D->MaxY, mExtent3D->MaxZ ); QgsDebugMsgLevel( QStringLiteral( "Finished get extent from 3D: (%1, %2, %3 : %4, %5, %6)" ) .arg( mExtentRect.xMinimum() ).arg( mExtentRect.yMinimum() ).arg( mExtentRect.zMinimum() ) .arg( mExtentRect.xMaximum() ).arg( mExtentRect.yMaximum() ).arg( mExtentRect.zMaximum() ), 3 ); } return mExtentRect.toRectangle(); } QgsBox3D QgsOgrProvider::extent3D() const { QgsDebugMsgLevel( QStringLiteral( "QgsOgrProvider::extent3D() starting" ), 3 ); if ( !elevationProperties()->containsElevationData() ) // ie. 2D { extent(); mExtentRect = QgsBox3D( mExtent2D->MinX, mExtent2D->MinY, std::numeric_limits::quiet_NaN(), mExtent2D->MaxX, mExtent2D->MaxY, std::numeric_limits::quiet_NaN() ); return mExtentRect; } if ( !mExtent3D ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); mExtent3D.reset( new OGREnvelope3D() ); // get the extent_ (envelope) of the layer QgsDebugMsgLevel( QStringLiteral( "Starting computing extent3D. subset: '%1'" ).arg( mSubsetString ), 3 ); if ( mOgrLayer == mOgrOrigLayer.get() ) { bool hasBeenComputed = false; mExtent3D->MinX = std::numeric_limits::max(); mExtent3D->MinY = std::numeric_limits::max(); mExtent3D->MinZ = std::numeric_limits::max(); mExtent3D->MaxX = -std::numeric_limits::max(); mExtent3D->MaxY = -std::numeric_limits::max(); mExtent3D->MaxZ = -std::numeric_limits::max(); if ( mForceRecomputeExtent && mValid && mWriteAccess && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) && mOgrOrigLayer ) { // works with unquoted layerName QByteArray sql = QByteArray( "RECOMPUTE EXTENT ON " ) + mOgrOrigLayer->name(); QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 ); mOgrOrigLayer->ExecuteSQLNoReturn( sql ); } else { OGREnvelope envelope2D; if ( mOgrLayer->GetExtent( &envelope2D, true ) == OGRERR_NONE ) // switch OLCFastGetExtent flag to TRUE { mExtent3D->MinX = envelope2D.MinX; mExtent3D->MinY = envelope2D.MinY; mExtent3D->MaxX = envelope2D.MaxX; mExtent3D->MaxY = envelope2D.MaxY; } } if ( !mOgrLayer->TestCapability( OLCFastGetExtent ) ) { QgsDebugMsgLevel( QStringLiteral( "WITHOUT OLCFastGetExtent" ), 3 ); if ( mGDALDriverName == QLatin1String( "OAPIF" ) || mGDALDriverName == QLatin1String( "WFS3" ) ) { QgsDebugMsgLevel( QStringLiteral( "WITHOUT OLCFastGetExtent AND is remote" ), 3 ); // When the extent is not in the metadata, retrieving it would be // super slow for remote data sources mExtent3D->MinX = -180; mExtent3D->MinY = -90; mExtent3D->MinZ = std::numeric_limits::quiet_NaN(); mExtent3D->MaxX = 180; mExtent3D->MaxY = 90; mExtent3D->MaxZ = std::numeric_limits::quiet_NaN(); hasBeenComputed = true; } // else slow computation } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,0) else // 3D (with or without subset) we delegate to gdal hasBeenComputed = mOgrLayer->GetExtent3D( mExtent3D.get(), true ) == OGRERR_NONE; // else only 2D with subset need slow computation #else else if ( mSubsetString.isEmpty() ) // mSubsetString is not properly handled by mOgrLayer->GetExtent3D if gdal<3.9.0 // we need to scan all feature (via QgsOgrLayer::computeExtent3DSlowly) to take ino account the mSubsetString filter { QgsDebugMsgLevel( QStringLiteral( "WITH OLCFastGetExtent" ), 3 ); hasBeenComputed = mOgrLayer->GetExtent3D( mExtent3D.get(), true ) == OGRERR_NONE; } #endif // else slow computation if ( !hasBeenComputed ) { QgsDebugMsgLevel( QStringLiteral( "will apply slow default 3D extent computing" ), 3 ); OGRErr err = mOgrLayer->computeExtent3DSlowly( mExtent3D.get() ); if ( err != OGRERR_NONE ) { QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute slow extent3D (ogr error: %1)" ).arg( err ), 1 ); mExtent3D.reset(); return QgsBox3D(); } } if ( mExtent3D->MinZ == std::numeric_limits::max() && mExtent3D->MaxZ == -std::numeric_limits::max() ) { QgsDebugMsgLevel( QStringLiteral( "is 2D/flat extent3D" ), 3 ); // flat extent mExtent3D->MinZ = std::numeric_limits::quiet_NaN(); mExtent3D->MaxZ = std::numeric_limits::quiet_NaN(); } } else { QgsDebugMsgLevel( QStringLiteral( "Failure: unable to compute extent3D (mOgrLayer != mOgrOrigLayer)" ), 1 ); mExtent3D.reset(); return QgsBox3D(); } } // ends if !mExtent3D QgsDebugMsgLevel( QStringLiteral( "Finished get extent3D: (%1, %2, %3 : %4, %5, %6)" ) .arg( mExtent3D->MinX ).arg( mExtent3D->MinY ).arg( mExtent3D->MinZ ) .arg( mExtent3D->MaxX ).arg( mExtent3D->MaxY ).arg( mExtent3D->MaxZ ), 3 ); mExtentRect = QgsBox3D( mExtent3D->MinX, mExtent3D->MinY, mExtent3D->MinZ, mExtent3D->MaxX, mExtent3D->MaxY, mExtent3D->MaxZ ); return mExtentRect; } QVariant QgsOgrProvider::defaultValue( int fieldId ) const { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( fieldId < 0 || fieldId >= fields().count() ) return QVariant(); QString defaultVal = mDefaultValues.value( fieldId, QString() ); if ( defaultVal.isEmpty() ) return QVariant(); QVariant resultVar = defaultVal; if ( defaultVal == QLatin1String( "CURRENT_TIMESTAMP" ) ) resultVar = QDateTime::currentDateTime(); else if ( defaultVal == QLatin1String( "CURRENT_DATE" ) ) resultVar = QDate::currentDate(); else if ( defaultVal == QLatin1String( "CURRENT_TIME" ) ) resultVar = QTime::currentTime(); // Get next sequence value for sqlite in case we are inside a transaction if ( mOgrOrigLayer && mTransaction && mDefaultValues.value( fieldId, QString() ) == tr( "Autogenerate" ) && providerProperty( EvaluateDefaultValues, false ).toBool() && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) && mFirstFieldIsFid && fieldId == 0 ) { QgsOgrLayerUniquePtr resultLayer = mOgrOrigLayer->ExecuteSQL( QByteArray( "SELECT seq FROM sqlite_sequence WHERE name = " ) + QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() ); if ( resultLayer ) { gdal::ogr_feature_unique_ptr f; if ( f.reset( resultLayer->GetNextFeature() ), f ) { bool ok { true }; const QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fields().at( 0 ), 0, textEncoding(), &ok ); if ( ok ) { long long nextVal { res.toLongLong( &ok ) }; if ( ok ) { // Increment resultVar = ++nextVal; mOgrOrigLayer->ExecuteSQLNoReturn( QByteArray( "UPDATE sqlite_sequence SET seq = seq + 1 WHERE name = " ) + QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() ); } } if ( ! ok ) { QgsMessageLog::logMessage( tr( "Error retrieving next sequence value for %1" ).arg( QString::fromUtf8( mOgrOrigLayer->name() ) ), tr( "OGR" ) ); } } else // no sequence! { resultVar = 1; mOgrOrigLayer->ExecuteSQLNoReturn( QByteArray( "INSERT INTO sqlite_sequence (name, seq) VALUES( " + QgsSqliteUtils::quotedValue( mOgrOrigLayer->name() ).toUtf8() ) + ", 1)" ); } } else { QgsMessageLog::logMessage( tr( "Error retrieving default value for %1" ).arg( mLayerName ), tr( "OGR" ) ); } } const bool compatible = fields().at( fieldId ).convertCompatible( resultVar ); return compatible && !QgsVariantUtils::isNull( resultVar ) ? resultVar : QVariant(); } QString QgsOgrProvider::defaultValueClause( int fieldIndex ) const { // Return empty clause to force defaultValue calls for sqlite in case we are inside a transaction if ( mTransaction && mDefaultValues.value( fieldIndex, QString() ) == tr( "Autogenerate" ) && providerProperty( EvaluateDefaultValues, false ).toBool() && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) && mFirstFieldIsFid && fieldIndex == 0 ) return QString(); else return mDefaultValues.value( fieldIndex, QString() ); } bool QgsOgrProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint constraint, const QVariant &value ) const { Q_UNUSED( constraint ) if ( providerProperty( EvaluateDefaultValues, false ).toBool() ) { return ! mDefaultValues.value( fieldIndex ).isEmpty(); } else { // stricter check return mDefaultValues.contains( fieldIndex ) && !QgsVariantUtils::isNull( value ) && ( mDefaultValues.value( fieldIndex ) == value.toString() || value.userType() == qMetaTypeId() ); } } void QgsOgrProvider::updateExtents() { invalidateCachedExtent( true ); } void QgsOgrProvider::invalidateCachedExtent( bool bForceRecomputeExtent ) { mForceRecomputeExtent = bForceRecomputeExtent; mExtent2D.reset(); mExtent3D.reset(); } size_t QgsOgrProvider::layerCount() const { if ( !mValid ) return 0; return mOgrLayer->GetLayerCount(); } /** * Returns the feature type */ Qgis::WkbType QgsOgrProvider::wkbType() const { Qgis::WkbType wkb = QgsOgrUtils::ogrGeometryTypeToQgsWkbType( mOGRGeomType ); const Qgis::WkbType wkbFlat = QgsWkbTypes::flatType( wkb ); if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && ( wkbFlat == Qgis::WkbType::LineString || wkbFlat == Qgis::WkbType::Polygon ) ) { wkb = QgsWkbTypes::multiType( wkb ); } return wkb; } /** * Returns the feature count */ long long QgsOgrProvider::featureCount() const { if ( ( mReadFlags & Qgis::DataProviderReadFlag::SkipFeatureCount ) != 0 ) { return static_cast< long long >( Qgis::FeatureCountState::UnknownCount ); } if ( mRefreshFeatureCount ) { mRefreshFeatureCount = false; recalculateFeatureCount(); } return mFeaturesCounted; } QgsFields QgsOgrProvider::fields() const { if ( mFieldsRequireReload ) { const_cast( this )->loadFields(); } return mAttributeFields; } //TODO - add sanity check for shape file layers, to include checking to // see if the .shp, .dbf, .shx files are all present and the layer // actually has features bool QgsOgrProvider::isValid() const { return mValid; } // Drivers may be more tolerant than we really wish (e.g. GeoPackage driver // may accept any geometry type) OGRGeometryH QgsOgrProvider::ConvertGeometryIfNecessary( OGRGeometryH hGeom ) { if ( !hGeom ) return hGeom; OGRwkbGeometryType layerGeomType = mOgrLayer->GetLayerDefn().GetGeomType(); OGRwkbGeometryType flattenLayerGeomType = wkbFlatten( layerGeomType ); OGRwkbGeometryType geomType = OGR_G_GetGeometryType( hGeom ); OGRwkbGeometryType flattenGeomType = wkbFlatten( geomType ); if ( flattenLayerGeomType == wkbUnknown || flattenLayerGeomType == flattenGeomType ) { return hGeom; } if ( flattenLayerGeomType == wkbMultiPolygon && flattenGeomType == wkbPolygon ) { return OGR_G_ForceToMultiPolygon( hGeom ); } if ( flattenLayerGeomType == wkbMultiLineString && flattenGeomType == wkbLineString ) { return OGR_G_ForceToMultiLineString( hGeom ); } if ( flattenLayerGeomType == wkbPolygon && flattenGeomType == wkbMultiPolygon && mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { // Do not force multipolygon to polygon for shapefiles, otherwise it will // cause issues with GDAL 3.7 that does honour the topological intent of // multipolygon return hGeom; } return OGR_G_ForceTo( hGeom, layerGeomType, nullptr ); } QString QgsOgrProvider::jsonStringValue( const QVariant &value ) const { QString stringValue = QString::fromUtf8( QJsonDocument::fromVariant( value ).toJson().constData() ); if ( stringValue.isEmpty() ) { //store as string, because it's no valid QJson value stringValue = value.toString(); } return stringValue; } // Stricter version of QVariant::toInt() that addresses the fact that // QVariant::toInt() doesn't detect integer truncation if the type // is a long long. static int strictToInt( const QVariant &v, bool *ok ) { if ( v.userType() == QMetaType::Type::Int ) { *ok = true; return v.toInt(); } else { const qlonglong val = v.toLongLong( ok ); if ( *ok && val >= std::numeric_limits::min() && val <= std::numeric_limits::max() ) { return static_cast( val ); } else { *ok = false; } } return 0; } // Converts a string with values "0", "false", "1", "true" to 0 / 1 static int stringToBool( const QString &strVal, bool *ok ) { if ( strVal.compare( QLatin1String( "0" ) ) == 0 || strVal.compare( QLatin1String( "false" ), Qt::CaseInsensitive ) == 0 ) { *ok = true; return 0; } else if ( strVal.compare( QLatin1String( "1" ) ) == 0 || strVal.compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0 ) { *ok = true; return 1; } *ok = false; return 0; } bool QgsOgrProvider::addFeaturePrivate( QgsFeature &f, Flags flags, QgsFeatureId incrementalFeatureId ) { bool returnValue = true; QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn(); gdal::ogr_feature_unique_ptr feature( featureDefinition.CreateFeature() ); if ( f.hasGeometry() ) { QByteArray wkb( f.geometry().asWkb() ); OGRGeometryH geom = nullptr; if ( !wkb.isEmpty() ) { if ( OGR_G_CreateFromWkb( reinterpret_cast( const_cast( wkb.constData() ) ), nullptr, &geom, wkb.length() ) != OGRERR_NONE ) { pushError( tr( "OGR error creating wkb for feature %1: %2" ).arg( f.id() ).arg( CPLGetLastErrorMsg() ) ); return false; } geom = ConvertGeometryIfNecessary( geom ); OGR_F_SetGeometryDirectly( feature.get(), geom ); } } QgsAttributes attributes = f.attributes(); const QgsFields qgisFields { f.fields() }; QgsLocaleNumC l; int qgisAttributeId = ( mFirstFieldIsFid ) ? 1 : 0; // If the first attribute is the FID and the user has set it, then use it if ( mFirstFieldIsFid && attributes.count() > 0 ) { QVariant attrFid = attributes.at( 0 ); if ( !QgsVariantUtils::isNull( attrFid ) ) { bool ok = false; qlonglong id = attrFid.toLongLong( &ok ); if ( ok ) { OGR_F_SetFID( feature.get(), static_cast( id ) ); } } } //add possible attribute information for ( int ogrAttributeId = 0; qgisAttributeId < attributes.count(); ++qgisAttributeId, ++ogrAttributeId ) { // Skip fields that have no provider origin if ( qgisFields.exists( qgisAttributeId ) && qgisFields.fieldOrigin( qgisAttributeId ) != Qgis::FieldOrigin::Provider ) { qgisAttributeId++; continue; } // don't try to set field from attribute map if it's not present in layer if ( ogrAttributeId >= featureDefinition.GetFieldCount() ) { pushError( tr( "Feature has too many attributes (expecting %1, received %2)" ).arg( featureDefinition.GetFieldCount() ).arg( f.attributeCount() ) ); continue; } //if(!s.isEmpty()) // continue; // OGRFieldDefnH fldDef = featureDefinition.GetFieldDefn( ogrAttributeId ); const QString ogrFieldName = textEncoding()->toUnicode( OGR_Fld_GetNameRef( fldDef ) ); const OGRFieldType type = OGR_Fld_GetType( fldDef ); const OGRFieldSubType subType = OGR_Fld_GetSubType( fldDef ); QVariant attrVal = attributes.at( qgisAttributeId ); const QMetaType::Type qType = static_cast( attrVal.userType() ); // The field value is equal to the default (that might be a provider-side expression) if ( attributes.isUnsetValue( qgisAttributeId ) || ( mDefaultValues.contains( qgisAttributeId ) && attrVal.toString() == mDefaultValues.value( qgisAttributeId ) ) ) { OGR_F_UnsetField( feature.get(), ogrAttributeId ); } else if ( QgsVariantUtils::isNull( attrVal ) || ( type != OFTString && ( ( qType != QMetaType::Type::QVariantList && attrVal.toString().isEmpty() && qType != QMetaType::Type::QStringList && attrVal.toStringList().isEmpty() ) || ( qType == QMetaType::Type::QVariantList && attrVal.toList().empty() ) ) ) ) { // Starting with GDAL 2.2, there are 2 concepts: unset fields and null fields // whereas previously there was only unset fields. For a GeoJSON output, // leaving a field unset will cause it to not appear at all in the output // feature. // When all features of a layer have a field unset, this would cause the // field to not be present at all in the output, and thus on reading to // have disappeared. #16812 #ifdef OGRNullMarker OGR_F_SetFieldNull( feature.get(), ogrAttributeId ); #else OGR_F_UnsetField( feature.get(), ogrAttId ); #endif } else { bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: { if ( subType == OFSTBoolean && qType == QMetaType::Type::QString ) { // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 const QString strVal = attrVal.toString(); OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, stringToBool( strVal, &ok ) ); if ( !ok ) { pushError( tr( "wrong value for attribute %1 of feature %2: %3" ) .arg( ogrFieldName ) .arg( f.id() ) .arg( strVal ) ); errorEmitted = true; } } else { OGR_F_SetFieldInteger( feature.get(), ogrAttributeId, strictToInt( attrVal, &ok ) ); } break; } case OFTInteger64: OGR_F_SetFieldInteger64( feature.get(), ogrAttributeId, attrVal.toLongLong( &ok ) ); break; case OFTReal: OGR_F_SetFieldDouble( feature.get(), ogrAttributeId, attrVal.toDouble( &ok ) ); break; case OFTDate: { const QDate date = attrVal.toDate(); if ( date.isValid() ) { ok = true; OGR_F_SetFieldDateTime( feature.get(), ogrAttributeId, date.year(), date.month(), date.day(), 0, 0, 0, 0 ); } break; } case OFTTime: { const QTime time = attrVal.toTime(); if ( time.isValid() ) { ok = true; OGR_F_SetFieldDateTimeEx( feature.get(), ogrAttributeId, 0, 0, 0, time.hour(), time.minute(), static_cast( time.second() + static_cast< double >( time.msec() ) / 1000 ), 0 ); } break; } case OFTDateTime: { QDateTime dt = attrVal.toDateTime(); if ( dt.isValid() ) { ok = true; if ( mConvertLocalTimeToUTC && dt.timeSpec() == Qt::LocalTime ) dt = dt.toUTC(); const QDate date = dt.date(); const QTime time = dt.time(); OGR_F_SetFieldDateTimeEx( feature.get(), ogrAttributeId, date.year(), date.month(), date.day(), time.hour(), time.minute(), static_cast( time.second() + static_cast< double >( time.msec() ) / 1000 ), QgsOgrUtils::OGRTZFlagFromQt( dt ) ); } break; } case OFTString: { ok = true; QString stringValue; if ( subType == OFSTJSON ) { stringValue = QString::fromStdString( QgsJsonUtils::jsonFromVariant( attrVal ).dump() ); } else { stringValue = attrVal.toString(); } QgsDebugMsgLevel( QStringLiteral( "Writing string attribute %1 with %2, encoding %3" ) .arg( qgisAttributeId ) .arg( attrVal.toString(), textEncoding()->name().data() ), 3 ); OGR_F_SetFieldString( feature.get(), ogrAttributeId, textEncoding()->fromUnicode( stringValue ).constData() ); break; } case OFTBinary: { const QByteArray ba = attrVal.toByteArray(); ok = true; OGR_F_SetFieldBinary( feature.get(), ogrAttributeId, ba.size(), const_cast< GByte * >( reinterpret_cast< const GByte * >( ba.data() ) ) ); break; } case OFTStringList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { QStringList list = attrVal.toStringList(); ok = true; int count = list.count(); char **lst = static_cast( CPLMalloc( ( count + 1 ) * sizeof( char * ) ) ); if ( count > 0 ) { int pos = 0; for ( const QString &string : list ) { lst[pos] = CPLStrdup( textEncoding()->fromUnicode( string ).data() ); pos++; } } lst[count] = nullptr; OGR_F_SetFieldStringList( feature.get(), ogrAttributeId, lst ); CSLDestroy( lst ); } break; } case OFTIntegerList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { const QVariantList list = attrVal.toList(); ok = true; const int count = list.count(); int *lst = new int[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = strictToInt( value, &ok ); if ( !ok ) break; pos++; } } if ( ok ) OGR_F_SetFieldIntegerList( feature.get(), ogrAttributeId, count, lst ); delete [] lst; } break; } case OFTRealList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { const QVariantList list = attrVal.toList(); ok = true; const int count = list.count(); double *lst = new double[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = value.toDouble( &ok ); if ( !ok ) break; pos++; } } if ( ok ) { OGR_F_SetFieldDoubleList( feature.get(), ogrAttributeId, count, lst ); } delete [] lst; } break; } case OFTInteger64List: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { const QVariantList list = attrVal.toList(); const int count = list.count(); long long *lst = new long long[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = value.toLongLong( &ok ); if ( !ok ) break; pos++; } } if ( ok ) { OGR_F_SetFieldInteger64List( feature.get(), ogrAttributeId, count, lst ); } delete [] lst; } break; } default: QgsMessageLog::logMessage( tr( "type %1 for attribute %2 not found" ).arg( type ).arg( qgisAttributeId ), tr( "OGR" ) ); break; } if ( !ok ) { if ( !errorEmitted ) { QMetaType::Type ogrVariantType = QMetaType::Type::UnknownType; QMetaType::Type ogrVariantSubType = QMetaType::Type::UnknownType; QgsOgrUtils::ogrFieldTypeToQVariantType( type, subType, ogrVariantType, ogrVariantSubType ); pushError( tr( "wrong data type for attribute %1 of feature %2: Got %3, expected %4" ) .arg( ogrFieldName ) .arg( f.id() ) .arg( attrVal.typeName(), QVariant::typeToName( ogrVariantType ) ) ); } returnValue = false; } } } if ( mOgrLayer->CreateFeature( feature.get() ) != OGRERR_NONE ) { pushError( tr( "OGR error creating feature %1: %2" ).arg( f.id() ).arg( CPLGetLastErrorMsg() ) ); returnValue = false; } else { if ( !( flags & QgsFeatureSink::FastInsert ) ) { QgsFeatureId id = static_cast( OGR_F_GetFID( feature.get() ) ); if ( id >= 0 ) { f.setId( id ); if ( mFirstFieldIsFid && attributes.count() > 0 ) { f.setAttribute( 0, id ); } } else if ( incrementalFeatureId >= 0 ) { f.setId( incrementalFeatureId ); } } } return returnValue; } bool QgsOgrProvider::addFeatures( QgsFeatureList &flist, Flags flags ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; setRelevantFields( true, attributeIndexes() ); const bool inTransaction = startTransaction(); QgsFeatureId incrementalFeatureId = -1; if ( !( flags & QgsFeatureSink::FastInsert ) && ( mGDALDriverName == QLatin1String( "CSV" ) || mGDALDriverName == QLatin1String( "XLSX" ) || mGDALDriverName == QLatin1String( "ODS" ) ) ) { QRecursiveMutex *mutex = nullptr; OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex ); { QMutexLocker locker( mutex ); if ( !mSubsetString.isEmpty() ) OGR_L_SetAttributeFilter( layer, nullptr ); incrementalFeatureId = static_cast< QgsFeatureId >( OGR_L_GetFeatureCount( layer, false ) ) + 1; if ( !mSubsetString.isEmpty() ) OGR_L_SetAttributeFilter( layer, textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ).constData() ); } } bool returnvalue = true; for ( QgsFeatureList::iterator it = flist.begin(); it != flist.end(); ++it ) { if ( !addFeaturePrivate( *it, flags, incrementalFeatureId ) ) { returnvalue = false; } if ( incrementalFeatureId >= 0 ) incrementalFeatureId++; } if ( inTransaction ) { if ( returnvalue ) returnvalue = commitTransaction(); else rollbackTransaction(); } if ( !syncToDisc() ) { returnvalue = false; } if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) ) { if ( returnvalue ) mFeaturesCounted += flist.size(); else recalculateFeatureCount(); } if ( returnvalue ) clearMinMaxCache(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); return returnvalue; } bool QgsOgrProvider::addAttributeOGRLevel( const QgsField &field, bool &ignoreErrorOut ) { ignoreErrorOut = false; OGRFieldType type; switch ( field.type() ) { case QMetaType::Type::Int: case QMetaType::Type::Bool: type = OFTInteger; break; case QMetaType::Type::LongLong: { const char *pszDataTypes = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_CREATIONFIELDDATATYPES, nullptr ); if ( pszDataTypes && strstr( pszDataTypes, "Integer64" ) ) type = OFTInteger64; else { type = OFTReal; } break; } case QMetaType::Type::Double: type = OFTReal; break; case QMetaType::Type::QDate: type = OFTDate; break; case QMetaType::Type::QTime: type = OFTTime; break; case QMetaType::Type::QDateTime: type = OFTDateTime; break; case QMetaType::Type::QString: type = OFTString; break; case QMetaType::Type::QByteArray: type = OFTBinary; break; case QMetaType::Type::QVariantMap: type = OFTString; break; case QMetaType::Type::QStringList: type = OFTStringList; break; case QMetaType::Type::QVariantList: if ( field.subType() == QMetaType::Type::QString ) { type = OFTStringList; break; } else if ( field.subType() == QMetaType::Type::Int ) { type = OFTIntegerList; break; } else if ( field.subType() == QMetaType::Type::LongLong ) { type = OFTInteger64List; break; } else if ( field.subType() == QMetaType::Type::Double ) { type = OFTRealList; break; } // other lists are supported at this moment, fall through to default for other types //intentional fall-through [[fallthrough]]; default: pushError( tr( "type %1 for field %2 not found" ).arg( field.typeName(), field.name() ) ); ignoreErrorOut = true; return false; } gdal::ogr_field_def_unique_ptr fielddefn( OGR_Fld_Create( textEncoding()->fromUnicode( field.name() ).constData(), type ) ); int width = field.length(); // Increase width by 1 for OFTReal to make room for the decimal point if ( type == OFTReal && field.precision() ) width += 1; OGR_Fld_SetWidth( fielddefn.get(), width ); OGR_Fld_SetPrecision( fielddefn.get(), field.precision() ); switch ( field.type() ) { case QMetaType::Type::Bool: OGR_Fld_SetSubType( fielddefn.get(), OFSTBoolean ); break; case QMetaType::Type::QVariantMap: OGR_Fld_SetSubType( fielddefn.get(), OFSTJSON ); break; default: break; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0) OGR_Fld_SetAlternativeName( fielddefn.get(), textEncoding()->fromUnicode( field.alias() ).constData() ); #endif #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) OGR_Fld_SetComment( fielddefn.get(), textEncoding()->fromUnicode( field.comment() ).constData() ); #endif if ( mOgrLayer->CreateField( fielddefn.get(), true ) != OGRERR_NONE ) { pushError( tr( "OGR error creating field %1: %2" ).arg( field.name(), CPLGetLastErrorMsg() ) ); return false; } return true; } bool QgsOgrProvider::addAttributes( const QList &attributes ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; if ( mGDALDriverName == QLatin1String( "MapInfo File" ) ) { // adding attributes in mapinfo requires to be able to delete the .dat file // so drop any cached connections. QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); } bool returnvalue = true; QMap< QString, QgsField > mapFieldNameToOriginalField; for ( const auto &field : attributes ) { mapFieldNameToOriginalField[ field.name()] = field; bool ignoreErrorOut = false; if ( !addAttributeOGRLevel( field, ignoreErrorOut ) ) { returnvalue = false; if ( !ignoreErrorOut ) { break; } } } // We at least want to syncToDisc() for OpenFileGDB, because its AddField // implementation doesn't update immediately system tables. // We exclude GeoJSON because leaveUpdateMode() has specific behavior for it. if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() ) { returnvalue = false; } // Backup existing fields. We need them to 'restore' field type, length, precision QgsFields oldFields = fields(); // Updates mAttributeFields loadFields(); // The check in QgsVectorLayerEditBuffer::commitChanges() is questionable with // real-world drivers that might only be able to satisfy request only partially. // So to avoid erroring out, patch field type, width and precision to match // what was requested. // For example in case of Integer64->Real mapping so that QVariant::LongLong is // still returned to the caller // Or if a field width was specified but not strictly enforced by the driver (#15614) for ( QMap< QString, QgsField >::const_iterator it = mapFieldNameToOriginalField.constBegin(); it != mapFieldNameToOriginalField.constEnd(); ++it ) { int idx = mAttributeFields.lookupField( it.key() ); if ( idx >= 0 ) { mAttributeFields[ idx ].setType( it->type() ); mAttributeFields[ idx ].setLength( it->length() ); mAttributeFields[ idx ].setPrecision( it->precision() ); mAttributeFields[ idx ].setEditorWidgetSetup( it->editorWidgetSetup() ); } } // Restore field type, length, precision of existing fields as well // We need that in scenarios where the user adds a int field with length != 0 // in a editing session, and repeat that again in another editing session // Without the below hack, the length of the first added field would have // been reset to zero, and QgsVectorLayerEditBuffer::commitChanges() would // error out because of this. // See https://github.com/qgis/QGIS/issues/26840 for ( const QgsField &field : oldFields ) { int idx = mAttributeFields.lookupField( field.name() ); if ( idx >= 0 ) { mAttributeFields[ idx ].setType( field.type() ); mAttributeFields[ idx ].setLength( field.length() ); mAttributeFields[ idx ].setPrecision( field.precision() ); } } if ( mTransaction ) mTransaction->dirtyLastSavePoint(); return returnvalue; } bool QgsOgrProvider::deleteAttributes( const QgsAttributeIds &attributes ) { if ( !doInitialActionsForEdition() ) return false; bool res = true; QList attrsLst( attributes.begin(), attributes.end() ); // sort in descending order std::sort( attrsLst.begin(), attrsLst.end(), std::greater() ); const auto constAttrsLst = attrsLst; for ( int attr : constAttrsLst ) { if ( mFirstFieldIsFid ) { if ( attr == 0 ) { pushError( tr( "Cannot delete feature id column" ) ); res = false; break; } else { --attr; } } if ( mOgrLayer->DeleteField( attr ) != OGRERR_NONE ) { pushError( tr( "OGR error deleting field %1: %2" ).arg( attr ).arg( CPLGetLastErrorMsg() ) ); res = false; } } // We at least want to syncToDisc() for OpenFileGDB, because its DeleteField // implementation doesn't update immediately system tables. // We exclude GeoJSON because leaveUpdateMode() has specific behavior for it. if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() ) { res = false; } loadFields(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); return res; } bool QgsOgrProvider::renameAttributes( const QgsFieldNameMap &renamedAttributes ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; QgsFieldNameMap::const_iterator renameIt = renamedAttributes.constBegin(); bool result = true; for ( ; renameIt != renamedAttributes.constEnd(); ++renameIt ) { int fieldIndex = renameIt.key(); if ( fieldIndex < 0 || fieldIndex >= fields().count() ) { pushError( tr( "Invalid attribute index" ) ); result = false; continue; } if ( fields().indexFromName( renameIt.value() ) >= 0 ) { //field name already in use pushError( tr( "Error renaming field %1: name '%2' already exists" ).arg( fieldIndex ).arg( renameIt.value() ) ); result = false; continue; } int ogrFieldIndex = fieldIndex; if ( mFirstFieldIsFid ) { ogrFieldIndex -= 1; if ( ogrFieldIndex < 0 ) { pushError( tr( "Invalid attribute index" ) ); result = false; continue; } } //type does not matter, it will not be used gdal::ogr_field_def_unique_ptr fld( OGR_Fld_Create( textEncoding()->fromUnicode( renameIt.value() ), OFTReal ) ); if ( mOgrLayer->AlterFieldDefn( ogrFieldIndex, fld.get(), ALTER_NAME_FLAG ) != OGRERR_NONE ) { pushError( tr( "OGR error renaming field %1: %2" ).arg( fieldIndex ).arg( CPLGetLastErrorMsg() ) ); result = false; } } // We at least want to syncToDisc() for OpenFileGDB, because its AlterFieldDefn // implementation doesn't update immediately system tables. // We exclude GeoJSON because leaveUpdateMode() has specific behavior for it. if ( mGDALDriverName != QLatin1String( "GeoJSON" ) && !syncToDisc() ) { result = false; } loadFields(); if ( mTransaction ) mTransaction->dirtyLastSavePoint(); return result; } bool QgsOgrProvider::startTransaction() { bool inTransaction = false; if ( !mTransaction && mOgrLayer->TestCapability( OLCTransactions ) ) { // A transaction might already be active, so be robust on failed // StartTransaction. CPLPushErrorHandler( CPLQuietErrorHandler ); inTransaction = ( mOgrLayer->StartTransaction() == OGRERR_NONE ); CPLPopErrorHandler(); } return inTransaction; } bool QgsOgrProvider::commitTransaction() { if ( mOgrLayer->CommitTransaction() != OGRERR_NONE ) { pushError( tr( "OGR error committing transaction: %1" ).arg( CPLGetLastErrorMsg() ) ); return false; } return true; } bool QgsOgrProvider::rollbackTransaction() { if ( mOgrLayer->RollbackTransaction() != OGRERR_NONE ) { pushError( tr( "OGR error rolling back transaction: %1" ).arg( CPLGetLastErrorMsg() ) ); return false; } return true; } bool QgsOgrProvider::_setSubsetString( const QString &theSQL, bool updateFeatureCount, bool updateCapabilities, bool hasExistingRef ) { QgsCPLErrorHandler handler( QObject::tr( "OGR" ) ); QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !mOgrOrigLayer ) return false; if ( theSQL == mSubsetString && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) ) return true; const QString oldSubsetString { mSubsetString }; const bool subsetStringHasChanged { theSQL != mSubsetString }; const QString cleanSql = QgsOgrProviderUtils::cleanSubsetString( theSQL ); if ( !cleanSql.isEmpty() ) { QRecursiveMutex *mutex = nullptr; OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex ); GDALDatasetH ds = mOgrOrigLayer->getDatasetHandleAndMutex( mutex ); OGRLayerH subsetLayerH; { QMutexLocker locker( mutex ); subsetLayerH = QgsOgrProviderUtils::setSubsetString( layer, ds, textEncoding(), cleanSql ); } if ( !subsetLayerH ) { pushError( tr( "OGR[%1] error %2: %3" ).arg( CPLGetLastErrorType() ).arg( CPLGetLastErrorNo() ).arg( CPLGetLastErrorMsg() ) ); return false; } if ( layer != subsetLayerH ) { mOgrSqlLayer = QgsOgrProviderUtils::getSqlLayer( mOgrOrigLayer.get(), subsetLayerH, cleanSql ); Q_ASSERT( mOgrSqlLayer.get() ); mOgrLayer = mOgrSqlLayer.get(); const QStringList tableNames {QgsOgrProviderUtils::tableNamesFromSelectSQL( cleanSql ) }; if ( ! tableNames.isEmpty() ) { mLayerName.clear(); } } else { mOgrSqlLayer.reset(); mOgrLayer = mOgrOrigLayer.get(); } } else { mOgrSqlLayer.reset(); mOgrLayer = mOgrOrigLayer.get(); QRecursiveMutex *mutex = nullptr; OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex ); { QMutexLocker locker( mutex ); OGR_L_SetAttributeFilter( layer, nullptr ); } // Try to guess the table name from the old subset string or we might // end with a layer URI without layername if ( !oldSubsetString.isEmpty() ) { const QStringList tableNames { QgsOgrProviderUtils::tableNamesFromSelectSQL( oldSubsetString ) }; if ( tableNames.size() > 0 ) { mLayerName = tableNames.at( 0 ); } } } mSubsetString = theSQL; QVariantMap parts; parts.insert( QStringLiteral( "path" ), mFilePath ); if ( !mLayerName.isNull() ) { parts.insert( QStringLiteral( "layerName" ), mLayerName ); } else if ( mIsSubLayer && mLayerIndex >= 0 ) { parts.insert( QStringLiteral( "layerId" ), mLayerIndex ); } if ( !mSubsetString.isEmpty() ) { parts.insert( QStringLiteral( "subset" ), mSubsetString ); } if ( mOgrGeometryTypeFilter != wkbUnknown ) { parts.insert( QStringLiteral( "geometryType" ), QgsOgrProviderUtils::ogrWkbGeometryTypeName( mOgrGeometryTypeFilter ) ); } if ( mUniqueGeometryType ) { parts.insert( QStringLiteral( "uniqueGeometryType" ), QStringLiteral( "yes" ) ); } if ( !mOpenOptions.isEmpty() ) { parts.insert( QStringLiteral( "openOptions" ), mOpenOptions ); } if ( !mCredentialOptions.isEmpty() ) { parts.insert( QStringLiteral( "credentialOptions" ), mCredentialOptions ); } QString uri = QgsOgrProviderMetadata().encodeUri( parts ); if ( uri != dataSourceUri() ) { if ( hasExistingRef ) QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); setDataSourceUri( uri ); if ( hasExistingRef ) QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); } mOgrLayer->ResetReading(); mRefreshFeatureCount = updateFeatureCount; // check the validity of the layer if subset string has changed if ( subsetStringHasChanged ) { loadFields(); } invalidateCachedExtent( false ); // Changing the filter may change capabilities if ( updateCapabilities ) computeCapabilities(); emit dataChanged(); return true; } bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_map ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; if ( attr_map.isEmpty() ) return true; bool returnValue = true; clearMinMaxCache(); setRelevantFields( true, attributeIndexes() ); const bool inTransaction = startTransaction(); // Some drivers may need to call ResetReading() after GetFeature(), such // as GPKG in GDAL < 2.3.0 to avoid letting the database in a locked state. // But this is undesirable in general, so don't do this when we know that // we don't need to. bool mayNeedResetReadingAfterGetFeature = true; if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) || mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "CSV" ) ) { mayNeedResetReadingAfterGetFeature = false; } QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn(); /* Optimization to update a single field of all layer's feature with a * constant value */ bool useUpdate = false; // If changing the below value, update it into test_provider_ogr_gpkg.py // as well constexpr size_t THRESHOLD_UPDATE_OPTIM = 100; if ( static_cast( attr_map.size() ) >= THRESHOLD_UPDATE_OPTIM && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) && mOgrLayer->TestCapability( OLCFastFeatureCount ) && attr_map.size() == mOgrLayer->GetFeatureCount() ) { std::set fids; OGRFieldDefnH fd = nullptr; int fieldIdx = -1; QVariant val; OGRFieldType type = OFTMaxType; useUpdate = true; for ( QgsChangedAttributesMap::const_iterator it = attr_map.begin(); it != attr_map.end(); ++it ) { QgsFeatureId fid = it.key(); fids.insert( fid ); const QgsAttributeMap &attr = it.value(); if ( attr.size() != 1 ) { useUpdate = false; break; } QgsAttributeMap::const_iterator it2 = attr.begin(); if ( fieldIdx < 0 ) { fieldIdx = it2.key(); if ( fieldIdx == 0 && mFirstFieldIsFid ) { useUpdate = false; break; } fd = featureDefinition.GetFieldDefn( ( mFirstFieldIsFid && fieldIdx > 0 ) ? fieldIdx - 1 : fieldIdx ); if ( !fd ) { useUpdate = false; break; } type = OGR_Fld_GetType( fd ); if ( type != OFTInteger && type != OFTInteger64 && type != OFTString && type != OFTReal ) { useUpdate = false; break; } val = *it2; } else if ( fieldIdx != it2.key() || val != *it2 ) { useUpdate = false; break; } } if ( useUpdate && fids.size() != static_cast( attr_map.size() ) ) { useUpdate = false; } if ( useUpdate && val.userType() != qMetaTypeId() ) { QString sql = QStringLiteral( "UPDATE %1 SET %2 = %3" ) .arg( QString::fromUtf8( QgsOgrProviderUtils::quotedIdentifier( mOgrLayer->name(), mGDALDriverName ) ) ) .arg( QString::fromUtf8( QgsOgrProviderUtils::quotedIdentifier( QByteArray( OGR_Fld_GetNameRef( fd ) ), mGDALDriverName ) ) ) .arg( QgsOgrProviderUtils::quotedValue( val ) ); QgsDebugMsgLevel( QStringLiteral( "Using optimized changeAttributeValues(): %1" ).arg( sql ), 3 ); CPLErrorReset(); mOgrOrigLayer->ExecuteSQLNoReturn( sql.toUtf8() ); if ( CPLGetLastErrorType() != CE_None ) { useUpdate = false; returnValue = false; } } } // General case: let's iterate over all features and attributes to update QgsChangedAttributesMap::const_iterator it = attr_map.begin(); if ( useUpdate ) it = attr_map.end(); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,3) // UpdateFeature is available since 3.7.0, but was broken for GeoJSON up to 3.9.3 // see https://github.com/OSGeo/gdal/pull/10197 // and https://github.com/OSGeo/gdal/pull/10794 const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature ); #elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature ) && mGDALDriverName != QLatin1String( "ODS" ) && mGDALDriverName != QLatin1String( "XLSX" ) && mGDALDriverName != QLatin1String( "GeoJSON" ); #else constexpr bool useUpdateFeature = false; #endif for ( ; it != attr_map.end(); ++it ) { QgsFeatureId fid = it.key(); const QgsAttributeMap &attr = it.value(); if ( attr.isEmpty() ) continue; gdal::ogr_feature_unique_ptr of; if ( useUpdateFeature ) { of.reset( featureDefinition.CreateFeature() ); OGR_F_SetFID( of.get(), FID_TO_NUMBER( fid ) ); } else { of.reset( mOgrLayer->GetFeature( FID_TO_NUMBER( fid ) ) ); if ( !of ) { pushError( tr( "Feature %1 for attribute update not found." ).arg( fid ) ); returnValue = false; continue; } if ( mayNeedResetReadingAfterGetFeature ) { mOgrLayer->ResetReading(); } } QgsLocaleNumC l; #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) QVector< int > changedFieldIndexes; changedFieldIndexes.reserve( attr.size() ); #endif for ( QgsAttributeMap::const_iterator it2 = attr.begin(); it2 != attr.end(); ++it2 ) { int f = it2.key(); if ( it2->userType() == qMetaTypeId< QgsUnsetAttributeValue >() ) continue; if ( mFirstFieldIsFid ) { if ( f == 0 ) { if ( it2->toLongLong() != fid ) { pushError( tr( "Changing feature id of feature %1 is not allowed." ).arg( fid ) ); returnValue = false; } continue; } else { --f; } } OGRFieldDefnH fd = OGR_F_GetFieldDefnRef( of.get(), f ); if ( !fd ) { pushError( tr( "Field %1 of feature %2 doesn't exist." ).arg( f ).arg( fid ) ); returnValue = false; continue; } OGRFieldType type = OGR_Fld_GetType( fd ); QMetaType::Type qType = static_cast( it2->userType() ); if ( QgsVariantUtils::isNull( *it2 ) || ( type != OFTString && ( ( qType != QMetaType::Type::QVariantList && qType != QMetaType::Type::QStringList && it2->toString().isEmpty() ) || ( qType == QMetaType::Type::QVariantList && it2->toList().empty() ) || ( qType == QMetaType::Type::QStringList && it2->toStringList().empty() ) ) ) ) { // Starting with GDAL 2.2, there are 2 concepts: unset fields and null fields // whereas previously there was only unset fields. For a GeoJSON output, // leaving a field unset will cause it to not appear at all in the output // feature. // When all features of a layer have a field unset, this would cause the // field to not be present at all in the output, and thus on reading to // have disappeared. #16812 #ifdef OGRNullMarker OGR_F_SetFieldNull( of.get(), f ); #else OGR_F_UnsetField( of.get(), f ); #endif #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } else { bool errorEmitted = false; bool ok = false; switch ( type ) { case OFTInteger: { if ( OGR_Fld_GetSubType( fd ) == OFSTBoolean && qType == QMetaType::Type::QString ) { // compatibility with use case of https://github.com/qgis/QGIS/issues/55517 const QString strVal = it2->toString(); OGR_F_SetFieldInteger( of.get(), f, stringToBool( strVal, &ok ) ); if ( !ok ) { pushError( tr( "wrong value for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( strVal ) ); errorEmitted = true; } else { #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } } else { OGR_F_SetFieldInteger( of.get(), f, strictToInt( *it2, &ok ) ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } break; } case OFTInteger64: OGR_F_SetFieldInteger64( of.get(), f, it2->toLongLong( &ok ) ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif break; case OFTReal: OGR_F_SetFieldDouble( of.get(), f, it2->toDouble( &ok ) ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif break; case OFTDate: { const QDate date = it2->toDate(); if ( date.isValid() ) { ok = true; OGR_F_SetFieldDateTime( of.get(), f, date.year(), date.month(), date.day(), 0, 0, 0, 0 ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } break; } case OFTTime: { const QTime time = it2->toTime(); if ( time.isValid() ) { ok = true; OGR_F_SetFieldDateTimeEx( of.get(), f, 0, 0, 0, time.hour(), time.minute(), static_cast( time.second() + static_cast< double >( time.msec() ) / 1000 ), 0 ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } break; } case OFTDateTime: { QDateTime dt = it2->toDateTime(); if ( dt.isValid() ) { ok = true; if ( mConvertLocalTimeToUTC && dt.timeSpec() == Qt::LocalTime ) dt = dt.toUTC(); const QDate date = dt.date(); const QTime time = dt.time(); OGR_F_SetFieldDateTimeEx( of.get(), f, date.year(), date.month(), date.day(), time.hour(), time.minute(), static_cast( time.second() + static_cast< double >( time.msec() ) / 1000 ), QgsOgrUtils::OGRTZFlagFromQt( dt ) ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } break; } case OFTString: { ok = true; QString stringValue; if ( OGR_Fld_GetSubType( fd ) == OFSTJSON ) { stringValue = QString::fromStdString( QgsJsonUtils::jsonFromVariant( it2.value() ).dump() ); } else { stringValue = jsonStringValue( it2.value() ); } OGR_F_SetFieldString( of.get(), f, textEncoding()->fromUnicode( stringValue ).constData() ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif break; } case OFTBinary: { ok = true; const QByteArray ba = it2->toByteArray(); OGR_F_SetFieldBinary( of.get(), f, ba.size(), const_cast< GByte * >( reinterpret_cast< const GByte * >( ba.data() ) ) ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif break; } case OFTStringList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { ok = true; QStringList list = it2->toStringList(); int count = list.count(); char **lst = static_cast( CPLMalloc( ( count + 1 ) * sizeof( char * ) ) ); if ( count > 0 ) { int pos = 0; for ( const QString &string : list ) { lst[pos] = CPLStrdup( textEncoding()->fromUnicode( string ).data() ); pos++; } } lst[count] = nullptr; OGR_F_SetFieldStringList( of.get(), f, lst ); CSLDestroy( lst ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } break; } case OFTIntegerList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { ok = true; const QVariantList list = it2->toList(); const int count = list.count(); int *lst = new int[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = strictToInt( value, &ok ); if ( !ok ) break; pos++; } } if ( ok ) { OGR_F_SetFieldIntegerList( of.get(), f, count, lst ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } delete [] lst; } break; } case OFTRealList: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { ok = true; const QVariantList list = it2->toList(); const int count = list.count(); double *lst = new double[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = value.toDouble( &ok ); if ( !ok ) break; pos++; } } if ( ok ) { OGR_F_SetFieldDoubleList( of.get(), f, count, lst ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } delete [] lst; } break; } case OFTInteger64List: { if ( qType == QMetaType::Type::QVariantList || qType == QMetaType::Type::QStringList ) { ok = true; const QVariantList list = it2->toList(); const int count = list.count(); long long *lst = new long long[count]; if ( count > 0 ) { int pos = 0; for ( const QVariant &value : list ) { lst[pos] = value.toLongLong( &ok ); if ( !ok ) break; pos++; } } if ( ok ) { OGR_F_SetFieldInteger64List( of.get(), f, count, lst ); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) changedFieldIndexes << f; #endif } delete [] lst; } break; } default: pushError( tr( "Type %1 of attribute %2 of feature %3 unknown." ).arg( type ).arg( it2.key() ).arg( fid ) ); returnValue = false; break; } if ( !ok ) { if ( !errorEmitted ) { pushError( tr( "wrong data type for attribute %1 of feature %2: %3" ).arg( it2.key() ) . arg( fid ) .arg( it2->typeName() ) ); } returnValue = false; } } } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) const OGRErr updateResult = useUpdateFeature ? mOgrLayer->UpdateFeature( of.get(), changedFieldIndexes.size(), changedFieldIndexes.constData(), 0, nullptr, false ) : mOgrLayer->SetFeature( of.get() ); if ( updateResult != OGRERR_NONE ) #else if ( mOgrLayer->SetFeature( of.get() ) != OGRERR_NONE ) #endif { pushError( tr( "OGR error setting feature %1: %2" ).arg( fid ).arg( CPLGetLastErrorMsg() ) ); returnValue = false; } } if ( inTransaction ) { if ( returnValue ) returnValue = commitTransaction(); else rollbackTransaction(); } if ( mTransaction ) mTransaction->dirtyLastSavePoint(); if ( !syncToDisc() ) { pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) ); } return returnValue; } bool QgsOgrProvider::changeGeometryValues( const QgsGeometryMap &geometry_map ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; setRelevantFields( true, attributeIndexes() ); const bool inTransaction = startTransaction(); // Some drivers may need to call ResetReading() after GetFeature(), such // as GPKG in GDAL < 2.3.0 to avoid letting the database in a locked state. // But this is undesirable in general, so don't do this when we know that // we don't need to. bool mayNeedResetReadingAfterGetFeature = true; if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { mayNeedResetReadingAfterGetFeature = false; } else if ( mGDALDriverName == QLatin1String( "GPKG" ) ) { mayNeedResetReadingAfterGetFeature = false; } QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn(); #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,9,1) const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature ); #elif GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) // see https://github.com/OSGeo/gdal/pull/10197 const bool useUpdateFeature = mOgrLayer->TestCapability( OLCUpdateFeature ) && mGDALDriverName != QLatin1String( "ODS" ) && mGDALDriverName != QLatin1String( "XLSX" ) && mGDALDriverName != QLatin1String( "GeoJSON" ); #else constexpr bool useUpdateFeature = false; #endif bool returnvalue = true; for ( QgsGeometryMap::const_iterator it = geometry_map.constBegin(); it != geometry_map.constEnd(); ++it ) { gdal::ogr_feature_unique_ptr theOGRFeature; if ( useUpdateFeature ) { theOGRFeature.reset( featureDefinition.CreateFeature() ); OGR_F_SetFID( theOGRFeature.get(), FID_TO_NUMBER( it.key() ) ); } else { theOGRFeature.reset( mOgrLayer->GetFeature( FID_TO_NUMBER( it.key() ) ) ); if ( !theOGRFeature ) { pushError( tr( "OGR error changing geometry: feature %1 not found" ).arg( it.key() ) ); returnvalue = false; continue; } if ( mayNeedResetReadingAfterGetFeature ) { mOgrLayer->ResetReading(); // needed for SQLite-based to clear iterator, which could let the database in a locked state otherwise } } OGRGeometryH newGeometry = nullptr; QByteArray wkb = it->asWkb(); // We might receive null geometries. It is OK, but don't go through the // OGR_G_CreateFromWkb() route then if ( !wkb.isEmpty() ) { //create an OGRGeometry if ( OGR_G_CreateFromWkb( reinterpret_cast( const_cast( wkb.constData() ) ), mOgrLayer->GetSpatialRef(), &newGeometry, wkb.length() ) != OGRERR_NONE ) { pushError( tr( "OGR error creating geometry for feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) ); OGR_G_DestroyGeometry( newGeometry ); returnvalue = false; continue; } if ( !newGeometry ) { pushError( tr( "OGR error in feature %1: geometry is null" ).arg( it.key() ) ); returnvalue = false; continue; } newGeometry = ConvertGeometryIfNecessary( newGeometry ); } //set the new geometry if ( OGR_F_SetGeometryDirectly( theOGRFeature.get(), newGeometry ) != OGRERR_NONE ) { pushError( tr( "OGR error setting geometry of feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) ); // Shouldn't happen normally. If it happens, ownership of the geometry // may be not really well defined, so better not destroy it, but just // the feature. returnvalue = false; continue; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) constexpr int firstIndex {0}; const OGRErr updateResult = useUpdateFeature ? mOgrLayer->UpdateFeature( theOGRFeature.get(), 0, nullptr, 1, &firstIndex, false ) : mOgrLayer->SetFeature( theOGRFeature.get() ); if ( updateResult != OGRERR_NONE ) #else if ( mOgrLayer->SetFeature( theOGRFeature.get() ) != OGRERR_NONE ) #endif { pushError( tr( "OGR error setting feature %1: %2" ).arg( it.key() ).arg( CPLGetLastErrorMsg() ) ); returnvalue = false; continue; } mShapefileMayBeCorrupted = true; invalidateCachedExtent( true ); } if ( inTransaction ) { if ( returnvalue ) returnvalue = commitTransaction(); else rollbackTransaction(); } if ( mTransaction ) mTransaction->dirtyLastSavePoint(); if ( !syncToDisc() ) { pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) ); } return returnvalue; } bool QgsOgrProvider::createSpatialIndexImpl() { QByteArray layerName = mOgrOrigLayer->name(); if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { QByteArray sql = QByteArray( "CREATE SPATIAL INDEX ON " ) + quotedIdentifier( layerName ); // quote the layer name so spaces are handled QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( QString::fromUtf8( sql ) ), 2 ); mOgrOrigLayer->ExecuteSQLNoReturn( sql ); if ( !mFilePath.endsWith( QLatin1String( ".shp" ), Qt::CaseInsensitive ) ) return true; QFileInfo fi( mFilePath ); // to get the base name //find out, if the .qix file is there mShapefileHadSpatialIndex = QFileInfo::exists( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".qix" ) ); return mShapefileHadSpatialIndex; } else if ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) { QRecursiveMutex *mutex = nullptr; OGRLayerH layer = mOgrOrigLayer->getHandleAndMutex( mutex ); QByteArray sql = QByteArray( "SELECT CreateSpatialIndex(" + quotedIdentifier( layerName ) + "," + quotedIdentifier( OGR_L_GetGeometryColumn( layer ) ) + ") " ); // quote the layer name so spaces are handled mOgrOrigLayer->ExecuteSQLNoReturn( sql ); return true; } return false; } bool QgsOgrProvider::createSpatialIndex() { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !mOgrOrigLayer ) return false; if ( !doInitialActionsForEdition() ) return false; return createSpatialIndexImpl(); } QString QgsOgrProvider::createIndexName( QString tableName, QString field ) { const thread_local QRegularExpression safeExp( QStringLiteral( "[^a-zA-Z0-9]" ) ); tableName.replace( safeExp, QStringLiteral( "_" ) ); field.replace( safeExp, QStringLiteral( "_" ) ); return tableName + "_" + field + "_idx"; } bool QgsOgrProvider::createAttributeIndex( int field ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( field < 0 || field >= fields().count() ) return false; if ( !doInitialActionsForEdition() ) return false; QByteArray quotedLayerName = quotedIdentifier( mOgrOrigLayer->name() ); if ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) { if ( field == 0 && mFirstFieldIsFid ) { // already an index on this field, no need to re-created return false; } QString indexName = createIndexName( mOgrOrigLayer->name(), fields().at( field ).name() ); QByteArray createSql = "CREATE INDEX IF NOT EXISTS " + textEncoding()->fromUnicode( indexName ) + " ON " + quotedLayerName + " (" + textEncoding()->fromUnicode( fields().at( field ).name() ) + ")"; mOgrOrigLayer->ExecuteSQLNoReturn( createSql ); return true; } else { QByteArray dropSql = "DROP INDEX ON " + quotedLayerName; mOgrOrigLayer->ExecuteSQLNoReturn( dropSql ); QByteArray createSql = "CREATE INDEX ON " + quotedLayerName + " USING " + textEncoding()->fromUnicode( fields().at( field ).name() ); mOgrOrigLayer->ExecuteSQLNoReturn( createSql ); QFileInfo fi( mFilePath ); // to get the base name //find out, if the .idm/.ind file is there QString idmFile( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".idm" ) ); QString indFile( fi.path().append( '/' ).append( fi.completeBaseName() ).append( ".ind" ) ); return QFile::exists( idmFile ) || QFile::exists( indFile ); } } bool QgsOgrProvider::deleteFeatures( const QgsFeatureIds &id ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; const bool inTransaction = startTransaction(); bool returnvalue = true; for ( QgsFeatureIds::const_iterator it = id.begin(); it != id.end(); ++it ) { if ( !deleteFeature( *it ) ) { returnvalue = false; } } if ( inTransaction ) { if ( returnvalue ) returnvalue = commitTransaction(); else rollbackTransaction(); } if ( mTransaction ) mTransaction->dirtyLastSavePoint(); if ( !syncToDisc() ) { returnvalue = false; } if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { // Shapefile behaves in a special way due to possible recompaction recalculateFeatureCount(); } else { if ( mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::Uncounted ) && mFeaturesCounted != static_cast< long long >( Qgis::FeatureCountState::UnknownCount ) ) { if ( returnvalue ) mFeaturesCounted -= id.size(); else recalculateFeatureCount(); } } clearMinMaxCache(); invalidateCachedExtent( true ); return returnvalue; } bool QgsOgrProvider::deleteFeature( QgsFeatureId id ) { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( !doInitialActionsForEdition() ) return false; if ( mOgrLayer->DeleteFeature( FID_TO_NUMBER( id ) ) != OGRERR_NONE ) { pushError( tr( "OGR error deleting feature %1: %2" ).arg( id ).arg( CPLGetLastErrorMsg() ) ); return false; } if ( mTransaction ) mTransaction->dirtyLastSavePoint(); mShapefileMayBeCorrupted = true; return true; } bool QgsOgrProvider::doInitialActionsForEdition() { if ( !mValid ) return false; // If mUpdateModeStackDepth > 0, it means that an updateMode is already active and that we have write access if ( mUpdateModeStackDepth == 0 ) { QgsDebugMsgLevel( QStringLiteral( "Enter update mode implicitly" ), 2 ); if ( !_enterUpdateMode( true ) ) return false; } mShapefileHadSpatialIndex = ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) && ( hasSpatialIndex() == Qgis::SpatialIndexPresence::Present ) ); return true; } Qgis::VectorProviderCapabilities QgsOgrProvider::capabilities() const { return mCapabilities; } Qgis::VectorDataProviderAttributeEditCapabilities QgsOgrProvider::attributeEditCapabilities() const { return mAttributeEditCapabilities; } QgsAttributeList QgsOgrProvider::pkAttributeIndexes() const { return mPrimaryKeyAttrs; } QString QgsOgrProvider::geometryColumnName() const { QGIS_PROTECT_QOBJECT_THREAD_ACCESS if ( !mOgrLayer ) return QString(); QgsOgrFeatureDefn &featureDefinition = mOgrLayer->GetLayerDefn(); if ( featureDefinition.GetGeomFieldCount() ) { OGRGeomFieldDefnH geomH = featureDefinition.GetGeomFieldDefn( 0 ); return QString::fromUtf8( OGR_GFld_GetNameRef( geomH ) ); } return QString(); } void QgsOgrProvider::computeCapabilities() { Qgis::VectorProviderCapabilities ability; bool updateModeActivated = false; // collect abilities reported by OGR if ( mOgrLayer ) { // We want the layer in rw mode or capabilities will be wrong // If mUpdateModeStackDepth > 0, it means that an updateMode is already active and that we have write access if ( mUpdateModeStackDepth == 0 ) { updateModeActivated = _enterUpdateMode( true ); } // Whilst the OGR documentation (e.g. at // https://gdal.org/doxygen/classOGRLayer.html#aeedbda1a62f9b89b8e5f24332cf22286) states "The capability // codes that can be tested are represented as strings, but #defined // constants exists to ensure correct spelling", we always use strings // here. This is because older versions of OGR don't always have all // the #defines we want to test for here. if ( mOgrLayer->TestCapability( "RandomRead" ) ) // true if the GetFeature() method works *efficiently* for this layer. // TODO: Perhaps influence if QGIS caches into memory // (vs read from disk every time) based on this setting. { // the latter flag is here just for compatibility ability |= Qgis::VectorProviderCapability::SelectAtId; } if ( mWriteAccessPossible && mOgrLayer->TestCapability( "SequentialWrite" ) ) // true if the CreateFeature() method works for this layer. { ability |= Qgis::VectorProviderCapability::AddFeatures; } if ( mWriteAccessPossible && mOgrLayer->TestCapability( "DeleteFeature" ) ) // true if this layer can delete its features { ability |= Qgis::VectorProviderCapability::DeleteFeatures; } if ( mWriteAccessPossible && mOgrLayer->TestCapability( "RandomWrite" ) ) // true if the SetFeature() method is operational on this layer. { // TODO According to http://shapelib.maptools.org/ (Shapefile C Library V1.2) // TODO "You can't modify the vertices of existing structures". // TODO Need to work out versions of shapelib vs versions of GDAL/OGR // TODO And test appropriately. ability |= Qgis::VectorProviderCapability::ChangeAttributeValues; ability |= Qgis::VectorProviderCapability::ChangeGeometries; } #if 0 if ( mOgrLayer->TestCapability( "FastFeatureCount" ) ) // true if this layer can return a feature count // (via OGRLayer::GetFeatureCount()) efficiently ... ie. without counting // the features. In some cases this will return true until a spatial // filter is installed after which it will return false. { // TODO: Perhaps use as a clue by QGIS whether it should spawn a thread to count features. } if ( mOgrLayer->TestCapability( "FastSetNextByIndex" ) ) // true if this layer can perform the SetNextByIndex() call efficiently. { // No use required for this QGIS release. } #endif if ( mWriteAccessPossible && mOgrLayer->TestCapability( "CreateField" ) ) { ability |= Qgis::VectorProviderCapability::AddAttributes; } if ( mWriteAccessPossible && mOgrLayer->TestCapability( "DeleteField" ) ) { ability |= Qgis::VectorProviderCapability::DeleteAttributes; } if ( mWriteAccessPossible && mOgrLayer->TestCapability( "AlterFieldDefn" ) ) { ability |= Qgis::VectorProviderCapability::RenameAttributes; } if ( !mOgrLayer->TestCapability( OLCStringsAsUTF8 ) ) { ability |= Qgis::VectorProviderCapability::SelectEncoding; } // OGR doesn't handle shapefiles without attributes, ie. missing DBFs well, fixes #803 if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { ability |= Qgis::VectorProviderCapability::CreateSpatialIndex; ability |= Qgis::VectorProviderCapability::CreateAttributeIndex; if ( ( ability & Qgis::VectorProviderCapability::ChangeAttributeValues ) == 0 ) { // on readonly shapes OGR reports that it can delete features although it can't RandomWrite ability.setFlag( Qgis::VectorProviderCapability::AddAttributes, false ); ability.setFlag( Qgis::VectorProviderCapability::DeleteFeatures, false ); } } else if ( mGDALDriverName == QLatin1String( "GPKG" ) ) { ability |= Qgis::VectorProviderCapability::CreateSpatialIndex; ability |= Qgis::VectorProviderCapability::CreateAttributeIndex; } else if ( mGDALDriverName == QLatin1String( "SQLite" ) ) { // Spatial index can only be created on Spatialite enabled datasources. QString sql = QStringLiteral( "SELECT 1 FROM sqlite_master WHERE name='spatialite_history' AND type='table'" ); bool isSpatialite = false; if ( QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql.toLocal8Bit().constData() ) ) { gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( f ) { isSpatialite = true; } } if ( isSpatialite ) ability |= Qgis::VectorProviderCapability::CreateSpatialIndex; ability |= Qgis::VectorProviderCapability::CreateAttributeIndex; } /* Curve geometries are available in some drivers starting with GDAL 2.0 */ if ( mOgrLayer->TestCapability( "CurveGeometries" ) ) { ability |= Qgis::VectorProviderCapability::CircularGeometries; } if ( mGDALDriverName == QLatin1String( "GPKG" ) ) { //supports transactions ability |= Qgis::VectorProviderCapability::TransactionSupport; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) if ( GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DCAP_FEATURE_STYLES_READ, nullptr ) ) #else // GDAL KML driver doesn't support reading feature style, skip metadata check until GDAL can separate reading/writing capability if ( mGDALDriverName != QLatin1String( "KML" ) && GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DCAP_FEATURE_STYLES, nullptr ) ) #endif { ability |= Qgis::VectorProviderCapability::FeatureSymbology; ability |= Qgis::VectorProviderCapability::CreateRenderer; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,7,0) if ( const char *pszAlterFieldDefnFlags = GDALGetMetadataItem( mOgrLayer->driver(), GDAL_DMD_CREATION_FIELD_DEFN_FLAGS, nullptr ) ) { char **papszTokens = CSLTokenizeString2( pszAlterFieldDefnFlags, " ", 0 ); if ( CSLFindString( papszTokens, "AlternativeName" ) >= 0 ) { mAttributeEditCapabilities |= Qgis::VectorDataProviderAttributeEditCapability::EditAlias; } if ( CSLFindString( papszTokens, "Comment" ) >= 0 ) { mAttributeEditCapabilities |= Qgis::VectorDataProviderAttributeEditCapability::EditComment; } CSLDestroy( papszTokens ); } #endif } ability |= Qgis::VectorProviderCapability::ReadLayerMetadata; ability |= Qgis::VectorProviderCapability::ReloadData; if ( updateModeActivated ) leaveUpdateMode(); mCapabilities = ability; } QString QgsOgrProvider::name() const { return TEXT_PROVIDER_KEY; } QString QgsOgrProvider::providerKey() { return TEXT_PROVIDER_KEY; } QString QgsOgrProvider::description() const { return TEXT_PROVIDER_DESCRIPTION; } QgsCoordinateReferenceSystem QgsOgrProvider::crs() const { QgsCoordinateReferenceSystem srs; if ( !mValid || ( mOGRGeomType == wkbNone ) ) return srs; if ( OGRSpatialReferenceH spatialRefSys = mOgrLayer->GetSpatialRef() ) { srs = QgsOgrUtils::OGRSpatialReferenceToCrs( spatialRefSys ); } else { QgsDebugMsgLevel( QStringLiteral( "no spatial reference found" ), 2 ); } return srs; } QString QgsOgrProvider::dataComment() const { if ( !mOgrLayer ) return QString(); // Potentially set on File Geodatabase return mOgrLayer->GetMetadataItem( QStringLiteral( "ALIAS_NAME" ) ); } QSet QgsOgrProvider::uniqueValues( int index, int limit ) const { QSet uniqueValues; if ( !mValid || index < 0 || index >= fields().count() ) return uniqueValues; const QgsField fld = fields().at( index ); if ( fld.name().isNull() ) { return uniqueValues; //not a provider field } QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); QByteArray sql = "SELECT DISTINCT " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ); // GPKG/SQLite fid // For GPKG and SQLITE drivers PK fields are not exposed as real fields, (and OGR_F_GetFID only // works with GPKG), so we are adding an extra column that will become index 0 // See https://github.com/qgis/QGIS/issues/29129 if ( ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) && mFirstFieldIsFid && index == 0 ) { sql += ", " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " AS fid2"; } sql += " FROM " + quotedIdentifier( mOgrLayer->name() ); if ( !mSubsetString.isEmpty() ) { sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ); } sql += " ORDER BY " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " ASC"; if ( limit > 0 ) { sql += " LIMIT " + QString::number( limit ).toLocal8Bit(); } QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ), 2 ); QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql ); if ( !l ) { QgsDebugError( QStringLiteral( "Failed to execute SQL" ) ); return QgsVectorDataProvider::uniqueValues( index, limit ); } gdal::ogr_feature_unique_ptr f; bool ok = false; while ( f.reset( l->GetNextFeature() ), f ) { const QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok ); if ( ok ) uniqueValues << res; if ( limit >= 0 && uniqueValues.size() >= limit ) break; } return uniqueValues; } QStringList QgsOgrProvider::uniqueStringsMatching( int index, const QString &substring, int limit, QgsFeedback *feedback ) const { QStringList results; if ( !mValid || index < 0 || index >= fields().count() ) return results; QgsField fld = fields().at( index ); if ( fld.name().isNull() ) { return results; //not a provider field } QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); // uniqueStringsMatching() is supposed to be case insensitive, so use the // ILIKE operator when it is available. // Prior to GDAL 3.1, with OGR SQL, LIKE behaved like ILIKE bool supportsILIKE = false; { QByteArray sql = "SELECT 1 FROM "; sql += quotedIdentifier( mOgrLayer->name() ); sql += " WHERE 'a' ILIKE 'A' LIMIT 1"; QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql ); if ( l ) { gdal::ogr_feature_unique_ptr f; f.reset( l->GetNextFeature() ); supportsILIKE = static_cast< bool>( f ); } } QByteArray sql = "SELECT DISTINCT " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ); sql += " FROM " + quotedIdentifier( mOgrLayer->name() ); sql += " WHERE " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ); if ( supportsILIKE ) sql += " ILIKE '%"; else sql += " LIKE '%"; sql += textEncoding()->fromUnicode( substring ) + "%'"; if ( !mSubsetString.isEmpty() ) { sql += " AND (" + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ) + ')'; } sql += " ORDER BY " + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ) + " ASC"; if ( limit > 0 ) { sql += " LIMIT " + QString::number( limit ).toLocal8Bit(); } QgsDebugMsgLevel( QStringLiteral( "SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ), 2 ); QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql ); if ( !l ) { QgsDebugError( QStringLiteral( "Failed to execute SQL" ) ); return QgsVectorDataProvider::uniqueStringsMatching( index, substring, limit, feedback ); } gdal::ogr_feature_unique_ptr f; while ( f.reset( l->GetNextFeature() ), f ) { if ( OGR_F_IsFieldSetAndNotNull( f.get(), 0 ) ) results << textEncoding()->toUnicode( OGR_F_GetFieldAsString( f.get(), 0 ) ); if ( ( limit >= 0 && results.size() >= limit ) || ( feedback && feedback->isCanceled() ) ) break; } return results; } Qgis::SpatialIndexPresence QgsOgrProvider::hasSpatialIndex() const { QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); if ( mOgrLayer && mOgrLayer->TestCapability( OLCFastSpatialFilter ) ) return Qgis::SpatialIndexPresence::Present; else if ( mOgrLayer ) return Qgis::SpatialIndexPresence::NotPresent; else return Qgis::SpatialIndexPresence::Unknown; } Qgis::VectorLayerTypeFlags QgsOgrProvider::vectorLayerTypeFlags() const { Qgis::VectorLayerTypeFlags flags; if ( mValid && mSubsetString.trimmed().startsWith( QStringLiteral( "SELECT" ), Qt::CaseSensitivity::CaseInsensitive ) ) { flags.setFlag( Qgis::VectorLayerTypeFlag::SqlQuery ); } return flags; } QList QgsOgrProvider::discoverRelations( const QgsVectorLayer *target, const QList &layers ) const { if ( !mOgrLayer ) return {}; #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,6,0) QList output; QRecursiveMutex *mutex = nullptr; GDALDatasetH hDS = mOgrLayer->getDatasetHandleAndMutex( mutex ); QMutexLocker locker( mutex ); char **relationNames = GDALDatasetGetRelationshipNames( hDS, nullptr ); if ( !relationNames ) return output; const QStringList names = QgsOgrUtils::cStringListToQStringList( relationNames ); CSLDestroy( relationNames ); QgsProviderMetadata *ogrProviderMetadata = QgsProviderRegistry::instance()->providerMetadata( TEXT_PROVIDER_KEY ); const QVariantMap thisLayerParts = ogrProviderMetadata->decodeUri( target->source() ); const QString thisPath = thisLayerParts.value( QStringLiteral( "path" ) ).toString(); for ( const QString &name : names ) { GDALRelationshipH relationship = GDALDatasetGetRelationship( hDS, name.toUtf8().constData() ); if ( !relationship ) continue; const QString leftTableName( GDALRelationshipGetLeftTableName( relationship ) ); if ( leftTableName != mLayerName ) continue; const QString rightTableName( GDALRelationshipGetRightTableName( relationship ) ); const QString mappingTableName( GDALRelationshipGetMappingTableName( relationship ) ); // try to find right and mapping tables in list of provided layers QgsVectorLayer *rightTableLayer = nullptr; QgsVectorLayer *mappingTableLayer = nullptr; for ( QgsVectorLayer *layer : layers ) { if ( layer->providerType() != TEXT_PROVIDER_KEY ) continue; const QVariantMap parts = ogrProviderMetadata->decodeUri( layer->source() ); if ( parts.value( QStringLiteral( "path" ) ).toString() != thisPath ) continue; if ( parts.value( QStringLiteral( "layerName" ) ).toString() == rightTableName ) rightTableLayer = layer; if ( parts.value( QStringLiteral( "layerName" ) ).toString() == mappingTableName ) mappingTableLayer = layer; } if ( !rightTableLayer ) continue; if ( !mappingTableName.isEmpty() && !mappingTableLayer ) continue; const QString relationshipName( GDALRelationshipGetName( relationship ) ); char **cslLeftTableFieldNames = GDALRelationshipGetLeftTableFields( relationship ); const QStringList leftTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslLeftTableFieldNames ); CSLDestroy( cslLeftTableFieldNames ); char **cslRightTableFieldNames = GDALRelationshipGetRightTableFields( relationship ); const QStringList rightTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslRightTableFieldNames ); CSLDestroy( cslRightTableFieldNames ); char **cslLeftMappingTableFieldNames = GDALRelationshipGetLeftMappingTableFields( relationship ); const QStringList leftMappingTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslLeftMappingTableFieldNames ); CSLDestroy( cslLeftMappingTableFieldNames ); char **cslRightMappingTableFieldNames = GDALRelationshipGetRightMappingTableFields( relationship ); const QStringList rightMappingTableFieldNames = QgsOgrUtils::cStringListToQStringList( cslRightMappingTableFieldNames ); CSLDestroy( cslRightMappingTableFieldNames ); const GDALRelationshipType relationshipType = GDALRelationshipGetType( relationship ); Qgis::RelationshipStrength strength = Qgis::RelationshipStrength::Association; switch ( relationshipType ) { case GRT_COMPOSITE: strength = Qgis::RelationshipStrength::Composition; break; case GRT_ASSOCIATION: strength = Qgis::RelationshipStrength::Association; break; case GRT_AGGREGATION: QgsLogger::warning( "Aggregation relationships are not supported" ); continue; } const GDALRelationshipCardinality cardinality = GDALRelationshipGetCardinality( relationship ); switch ( cardinality ) { case GRC_ONE_TO_ONE: case GRC_ONE_TO_MANY: case GRC_MANY_TO_ONE: { QgsRelation relation; relation.setName( relationshipName ); relation.setId( relationshipName ); relation.setReferencedLayer( target->id() ); relation.setReferencingLayer( rightTableLayer->id() ); relation.setStrength( strength ); for ( int i = 0; i < std::min( leftTableFieldNames.length(), rightTableFieldNames.length() ); ++i ) { relation.addFieldPair( rightTableFieldNames.at( i ), leftTableFieldNames.at( i ) ); } if ( relation.isValid() ) { output.append( relation ); } else { QgsLogger::warning( "Invalid relation for " + relationshipName ); } break; } case GRC_MANY_TO_MANY: { if ( !mappingTableLayer ) { QgsLogger::warning( "No mapping table for " + relationshipName ); continue; } QgsRelation relation; relation.setName( relationshipName ); relation.setId( relationshipName + "_forward" ); relation.setReferencedLayer( target->id() ); relation.setReferencingLayer( mappingTableLayer->id() ); relation.setStrength( strength ); for ( int i = 0; i < std::min( leftTableFieldNames.length(), leftMappingTableFieldNames.length() ); ++i ) { relation.addFieldPair( leftMappingTableFieldNames.at( i ), leftTableFieldNames.at( i ) ); } if ( relation.isValid() ) { output.append( relation ); } else { QgsLogger::warning( "Invalid relation for " + relationshipName ); continue; } QgsRelation relation2; relation2.setName( relationshipName ); relation2.setId( relationshipName + "_backward" ); relation2.setReferencedLayer( rightTableLayer->id() ); relation2.setReferencingLayer( mappingTableLayer->id() ); relation2.setStrength( strength ); for ( int i = 0; i < std::min( rightTableFieldNames.length(), rightMappingTableFieldNames.length() ); ++i ) { relation2.addFieldPair( rightMappingTableFieldNames.at( i ), rightTableFieldNames.at( i ) ); } if ( relation2.isValid() ) { output.append( relation2 ); } else { QgsLogger::warning( "Invalid relation for " + relationshipName ); continue; } break; } } } return output; #else Q_UNUSED( target ) Q_UNUSED( layers ) return {}; #endif } QVariant QgsOgrProvider::minimumValue( int index ) const { if ( !mValid || index < 0 || index >= fields().count() ) { return QVariant(); } QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); const QgsField originalField = fields().at( index ); QgsField fld = originalField; // can't use native date/datetime types -- OGR converts these to string in the MAX return value if ( fld.type() == QMetaType::Type::QDateTime || fld.type() == QMetaType::Type::QDate ) { fld.setType( QMetaType::Type::QString ); } // Don't quote column name (see https://trac.osgeo.org/gdal/ticket/5799#comment:9) QByteArray sql = "SELECT MIN(" + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ); sql += ") FROM " + quotedIdentifier( mOgrLayer->name() ); if ( !mSubsetString.isEmpty() ) { sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ); } QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql ); if ( !l ) { QgsDebugError( QStringLiteral( "Failed to execute SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ) ); return QgsVectorDataProvider::minimumValue( index ); } gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( !f ) { return QVariant(); } bool ok = false; QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok ); if ( !ok ) return QVariant(); if ( res.userType() != originalField.type() ) res = convertValue( originalField.type(), res.toString() ); if ( originalField.type() == QMetaType::Type::QDateTime ) { // ensure that we treat times as local time, to match behavior when iterating features QDateTime temp = res.toDateTime(); temp.setTimeSpec( Qt::LocalTime ); res = temp; } return res; } QVariant QgsOgrProvider::maximumValue( int index ) const { if ( !mValid || index < 0 || index >= fields().count() ) { return QVariant(); } QgsCPLHTTPFetchOverrider oCPLHTTPFetcher( mAuthCfg ); QgsSetCPLHTTPFetchOverriderInitiatorClass( oCPLHTTPFetcher, QStringLiteral( "QgsOgrProvider" ) ); const QgsField originalField = fields().at( index ); QgsField fld = originalField; // can't use native date/datetime types -- OGR converts these to string in the MAX return value if ( fld.type() == QMetaType::Type::QDateTime || fld.type() == QMetaType::Type::QDate ) { fld.setType( QMetaType::Type::QString ); } // Don't quote column name (see https://trac.osgeo.org/gdal/ticket/5799#comment:9) QByteArray sql = "SELECT MAX(" + quotedIdentifier( textEncoding()->fromUnicode( fld.name() ) ); sql += ") FROM " + quotedIdentifier( mOgrLayer->name() ); if ( !mSubsetString.isEmpty() ) { sql += " WHERE " + textEncoding()->fromUnicode( QgsOgrProviderUtils::cleanSubsetString( mSubsetString ) ); } QgsOgrLayerUniquePtr l = mOgrLayer->ExecuteSQL( sql ); if ( !l ) { QgsDebugError( QStringLiteral( "Failed to execute SQL: %1" ).arg( textEncoding()->toUnicode( sql ) ) ); return QgsVectorDataProvider::maximumValue( index ); } gdal::ogr_feature_unique_ptr f( l->GetNextFeature() ); if ( !f ) { return QVariant(); } bool ok = false; QVariant res = QgsOgrUtils::getOgrFeatureAttribute( f.get(), fld, 0, textEncoding(), &ok ); if ( !ok ) return QVariant(); if ( res.userType() != originalField.type() ) res = convertValue( originalField.type(), res.toString() ); if ( originalField.type() == QMetaType::Type::QDateTime ) { // ensure that we treat times as local time, to match behavior when iterating features QDateTime temp = res.toDateTime(); temp.setTimeSpec( Qt::LocalTime ); res = temp; } return res; } QByteArray QgsOgrProvider::quotedIdentifier( const QByteArray &field ) const { return QgsOgrProviderUtils::quotedIdentifier( field, mGDALDriverName ); } bool QgsOgrProvider::syncToDisc() { QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); //for shapefiles, remove spatial index files and create a new index QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); bool shapeIndex = false; if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { QString sbnIndexFile; QFileInfo fi( mFilePath ); int suffixLength = fi.suffix().length(); sbnIndexFile = mFilePath; sbnIndexFile.chop( suffixLength ); sbnIndexFile.append( "sbn" ); if ( QFile::exists( sbnIndexFile ) ) { shapeIndex = true; close(); QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); QFile::remove( sbnIndexFile ); open( OpenModeSameAsCurrent ); if ( !mValid ) return false; } } if ( mOgrLayer->SyncToDisk() != OGRERR_NONE ) { pushError( tr( "OGR error syncing to disk: %1" ).arg( CPLGetLastErrorMsg() ) ); } QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); if ( shapeIndex ) { return createSpatialIndex(); } return true; } void QgsOgrProvider::recalculateFeatureCount() const { if ( !mOgrLayer ) { mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::Uncounted ); return; } OGRGeometryH filter = mOgrLayer->GetSpatialFilter(); if ( filter ) { filter = OGR_G_Clone( filter ); mOgrLayer->SetSpatialFilter( nullptr ); } // feature count returns number of features within current spatial filter // so we remove it if there's any and then put it back if ( mOgrGeometryTypeFilter == wkbUnknown || mUniqueGeometryType ) { mFeaturesCounted = mOgrLayer->GetApproxFeatureCount(); if ( mFeaturesCounted == -1 ) { mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::UnknownCount ); } } else { mFeaturesCounted = 0; setRelevantFields( true, QgsAttributeList() ); mOgrLayer->ResetReading(); gdal::ogr_feature_unique_ptr fet; const OGRwkbGeometryType flattenGeomTypeFilter = QgsOgrProviderUtils::ogrWkbSingleFlattenAndLinear( mOgrGeometryTypeFilter ); while ( fet.reset( mOgrLayer->GetNextFeature() ), fet ) { OGRGeometryH geom = OGR_F_GetGeometryRef( fet.get() ); if ( geom ) { OGRwkbGeometryType gType = OGR_G_GetGeometryType( geom ); gType = QgsOgrProviderUtils::ogrWkbSingleFlattenAndLinear( gType ); if ( gType == flattenGeomTypeFilter ) mFeaturesCounted++; } } mOgrLayer->ResetReading(); setRelevantFields( true, attributeIndexes() ); } if ( filter ) { mOgrLayer->SetSpatialFilter( filter ); } QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); } bool QgsOgrProvider::doesStrictFeatureTypeCheck() const { // FIXME probably other drivers too... return mGDALDriverName != QLatin1String( "ESRI Shapefile" ) || ( mOGRGeomType == wkbPoint || mOGRGeomType == wkbPoint25D ); } QgsFeatureRenderer *QgsOgrProvider::createRenderer( const QVariantMap & ) const { if ( !( mCapabilities & Qgis::VectorProviderCapability::FeatureSymbology ) ) return nullptr; std::unique_ptr< QgsSymbol > defaultSymbol( QgsSymbol::defaultSymbol( QgsWkbTypes::geometryType( wkbType() ) ) ); return new QgsEmbeddedSymbolRenderer( defaultSymbol.release() ); } void QgsOgrProvider::open( OpenMode mode ) { bool openReadOnly = false; Q_ASSERT( !mOgrSqlLayer ); Q_ASSERT( !mOgrOrigLayer ); // Try to open using VSIFileHandler // see http://trac.osgeo.org/gdal/wiki/UserDocs/ReadInZip const QString vsiPrefix = QgsGdalUtils::vsiPrefixForPath( dataSourceUri( true ) ); if ( ( !vsiPrefix.isEmpty() && vsiPrefix != QLatin1String( "/vsimem/" ) ) || mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) ) { // GDAL>=1.8.0 has write support for zip, but read and write operations // cannot be interleaved, so for now just use read-only. openReadOnly = true; if ( !mFilePath.startsWith( vsiPrefix ) ) { mFilePath = vsiPrefix + mFilePath; setDataSourceUri( mFilePath ); } QgsDebugMsgLevel( QStringLiteral( "Trying %1 syntax, mFilePath= %2" ).arg( vsiPrefix, mFilePath ), 2 ); } QgsDebugMsgLevel( "mFilePath: " + mFilePath, 3 ); QgsDebugMsgLevel( "mLayerIndex: " + QString::number( mLayerIndex ), 3 ); QgsDebugMsgLevel( "mLayerName: " + mLayerName, 3 ); QgsDebugMsgLevel( "mSubsetString: " + mSubsetString, 3 ); CPLSetConfigOption( "GPX_ELE_AS_25D", "YES" ); // use GPX elevation as z values CPLSetConfigOption( "LIBKML_RESOLVE_STYLE", "YES" ); // resolve kml style urls from style tables to feature style strings if ( !CPLGetConfigOption( "OSM_USE_CUSTOM_INDEXING", nullptr ) ) { // Disable custom/fast indexing by default, as it can prevent some .osm.pbf // files to be loaded. // See https://github.com/qgis/QGIS/issues/31062 CPLSetConfigOption( "OSM_USE_CUSTOM_INDEXING", "NO" ); } if ( mFilePath.startsWith( QLatin1String( "MySQL:" ) ) && !mLayerName.isEmpty() && !mFilePath.endsWith( ",tables=" + mLayerName ) ) { mFilePath += ",tables=" + mLayerName; } if ( mode == OpenModeForceReadOnly ) openReadOnly = true; else if ( mode == OpenModeSameAsCurrent && !mWriteAccess ) openReadOnly = true; // Do not try to open FileGeodatabase in update mode if not possible (cf https://github.com/OSGeo/gdal/pull/8075) const QString fileSuffix( QFileInfo( mFilePath ).suffix() ); if ( !openReadOnly && fileSuffix.compare( QLatin1String( "gdb" ), Qt::CaseInsensitive ) == 0 ) { QFileInfo fiCatalogTable( mFilePath, "a00000001.gdbtable" ); if ( fiCatalogTable.exists() && !fiCatalogTable.isWritable() ) { openReadOnly = true; } } const bool bIsGpkg = fileSuffix.compare( QLatin1String( "gpkg" ), Qt::CaseInsensitive ) == 0; #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2) bool forceWAL = false; #endif // first try to open in update mode (unless specified otherwise) QString errCause; if ( !openReadOnly ) { QStringList options( mOpenOptions ); if ( !bIsGpkg && ( mode == OpenModeForceUpdateRepackOff || ( mDeferRepack && OpenModeSameAsCurrent ) ) ) { options << "AUTO_REPACK=OFF"; } #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2) if ( bIsGpkg && options.contains( "QGIS_FORCE_WAL=ON" ) ) { // Hack use for qgsofflineediting / https://github.com/qgis/QGIS/issues/48012 forceWAL = true; } if ( bIsGpkg && mode == OpenModeInitial && !forceWAL && QgsOgrProviderUtils::IsLocalFile( mFilePath ) ) { // A hint to QgsOgrProviderUtils::GDALOpenWrapper() to not force WAL // as in OpenModeInitial we are not going to do anything besides getting capabilities // and re-opening in readonly mode. options << "DO_NOT_ENABLE_WAL=ON"; } #endif // We get the layer which was requested by the uri. The layername // has precedence over the layerid if both are given. if ( !mLayerName.isNull() ) { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, options, mLayerName, errCause, true ); } else { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, true, options, mLayerIndex, errCause, true ); } } mValid = false; if ( mOgrOrigLayer ) { mWriteAccess = true; mWriteAccessPossible = true; } else { mWriteAccess = false; if ( !openReadOnly ) { QgsDebugMsgLevel( QStringLiteral( "OGR failed to opened in update mode, trying in read-only mode" ), 2 ); } QStringList options( mOpenOptions ); // assume trusted data to get more speed if ( mGDALDriverName == QLatin1String( "FlatGeobuf" ) && !options.contains( QStringLiteral( "VERIFY_BUFFERS=YES" ) ) ) { options << QStringLiteral( "VERIFY_BUFFERS=NO" ); } // try to open read-only if ( !mLayerName.isNull() ) { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, options, mLayerName, errCause, true ); } else { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, options, mLayerIndex, errCause, true ); } } if ( mOgrOrigLayer ) { mGDALDriverName = mOgrOrigLayer->driverName(); mShareSameDatasetAmongLayers = QgsOgrProviderUtils::canDriverShareSameDatasetAmongLayers( mGDALDriverName ); // Should we set it to true unconditionally? as OGR doesn't do any time // zone conversion for local time. For now, only do that for GeoPackage // since it requires UTC. mConvertLocalTimeToUTC = ( mGDALDriverName == QLatin1String( "GPKG" ) ); QgsDebugMsgLevel( "OGR opened using Driver " + mGDALDriverName, 2 ); mOgrLayer = mOgrOrigLayer.get(); // check that the initial encoding setting is fit for this layer if ( mode == OpenModeInitial && mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { // determine encoding from shapefile cpg or LDID information, if possible QString shpEncoding; shpEncoding = mOgrLayer->GetMetadataItem( QStringLiteral( "ENCODING_FROM_CPG" ), QStringLiteral( "SHAPEFILE" ) ); if ( shpEncoding.isEmpty() ) shpEncoding = mOgrLayer->GetMetadataItem( QStringLiteral( "ENCODING_FROM_LDID" ), QStringLiteral( "SHAPEFILE" ) ); if ( !shpEncoding.isEmpty() ) setEncoding( shpEncoding ); else setEncoding( encoding() ); } else { setEncoding( encoding() ); } // Ensure subset is set (setSubsetString does nothing if the passed sql subset string is equal to mSubsetString, which is the case when reloading the dataset) QString origSubsetString = mSubsetString; mSubsetString.clear(); // Block signals to avoid endless recursion reloadData -> emit dataChanged -> reloadData blockSignals( true ); // Do not update capabilities: it will be done later // WARNING if this is the initial open - we don't already have a connection ref, and will be creating one later. So we *mustn't* grab an extra connection ref // while setting the subset string, or we'll be left with an extra reference which is never cleared. mValid = _setSubsetString( origSubsetString, false, false, mode != OpenModeInitial ); blockSignals( false ); if ( mValid ) { if ( mode == OpenModeInitial ) { computeCapabilities(); } QgsDebugMsgLevel( QStringLiteral( "Data source is valid" ), 2 ); } else { QgsMessageLog::logMessage( tr( "Data source is invalid (%1)" ).arg( QString::fromUtf8( CPLGetLastErrorMsg() ) ), tr( "OGR" ) ); } } else { QgsMessageLog::logMessage( errCause + "(" + QString::fromUtf8( CPLGetLastErrorMsg() ) + ")", tr( "OGR" ) ); } // For shapefiles or MapInfo .tab, so as to allow concurrent opening between // QGIS and MapInfo, we go back to read-only mode for now. // For GPKG too, so to open in read-only nolock mode for GDAL >= 3.4.2 // We limit to those drivers as re-opening is relatively cheap (other drivers // like GeoJSON might do full content ingestion for example) if ( mValid && mode == OpenModeInitial && mWriteAccess && ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) || mGDALDriverName == QLatin1String( "MapInfo File" ) #if GDAL_VERSION_NUM >= GDAL_COMPUTE_VERSION(3,4,2) || ( !forceWAL && mGDALDriverName == QLatin1String( "GPKG" ) && QgsOgrProviderUtils::IsLocalFile( mFilePath ) ) #endif ) ) { mOgrSqlLayer.reset(); mOgrOrigLayer.reset(); mOgrLayer = nullptr; mValid = false; // In the case where we deal with a shapefile, it is possible that it has // pre-existing holes in the DBF (see #15407), so do a packing at the first edit // action. if ( mGDALDriverName == QLatin1String( "ESRI Shapefile" ) ) { mShapefileMayBeCorrupted = true; } // try to open read-only if ( !mLayerName.isNull() ) { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, mOpenOptions, mLayerName, errCause, true ); } else { mOgrOrigLayer = QgsOgrProviderUtils::getLayer( mFilePath, false, mOpenOptions, mLayerIndex, errCause, true ); } mWriteAccess = false; mOgrLayer = mOgrOrigLayer.get(); if ( mOgrLayer ) { mValid = true; mDynamicWriteAccess = true; if ( !mSubsetString.isEmpty() ) { // Do not update capabilities here // but ensure subset is set (setSubsetString does nothing if the passed sql subset string is equal to // mSubsetString, which is the case when reloading the dataset) QString origSubsetString = mSubsetString; mSubsetString.clear(); mValid = _setSubsetString( origSubsetString, false, false ); } } else { QgsMessageLog::logMessage( tr( "Cannot reopen %1: %2" ).arg( mFilePath ).arg( errCause ), tr( "OGR" ) ); } } // For debug/testing purposes if ( !mValid ) setProperty( "_debug_open_mode", "invalid" ); else if ( mWriteAccess ) setProperty( "_debug_open_mode", "read-write" ); else setProperty( "_debug_open_mode", "read-only" ); mRefreshFeatureCount = true; } void QgsOgrProvider::close() { if ( mWriteAccess && mForceRecomputeExtent ) extent(); mOgrSqlLayer.reset(); mOgrOrigLayer.reset(); mOgrLayer = nullptr; mValid = false; setProperty( "_debug_open_mode", "invalid" ); invalidateCachedExtent( false ); } void QgsOgrProvider::invalidateNetworkCache() { if ( mFilePath.startsWith( QLatin1String( "/vsicurl/" ) ) || mFilePath.startsWith( QLatin1String( "/vsis3/" ) ) || mFilePath.startsWith( QLatin1String( "/vsigs/" ) ) || mFilePath.startsWith( QLatin1String( "/vsiaz/" ) ) || mFilePath.startsWith( QLatin1String( "/vsiadls/" ) ) ) { QgsDebugMsgLevel( QString( "Invalidating cache for %1" ).arg( mFilePath ), 3 ); VSICurlPartialClearCache( mFilePath.toUtf8().constData() ); } } void QgsOgrProvider::reloadProviderData() { invalidateNetworkCache(); mFeaturesCounted = static_cast< long long >( Qgis::FeatureCountState::Uncounted ); bool wasValid = mValid; QgsOgrConnPool::instance()->invalidateConnections( QgsOgrProviderUtils::connectionPoolId( dataSourceUri( true ), mShareSameDatasetAmongLayers ) ); close(); open( OpenModeSameAsCurrent ); if ( !mValid && wasValid ) pushError( tr( "Cannot reopen datasource %1" ).arg( dataSourceUri() ) ); } bool QgsOgrProvider::_enterUpdateMode( bool implicit ) { if ( !mWriteAccessPossible ) { return false; } if ( mWriteAccess ) { ++mUpdateModeStackDepth; return true; } if ( mUpdateModeStackDepth == 0 ) { Q_ASSERT( mDynamicWriteAccess ); QgsDebugMsgLevel( QStringLiteral( "Reopening %1 in update mode" ).arg( dataSourceUri() ), 2 ); close(); open( implicit ? OpenModeForceUpdate : OpenModeForceUpdateRepackOff ); if ( !mOgrLayer || !mWriteAccess ) { QgsMessageLog::logMessage( tr( "Cannot reopen datasource %1 in update mode" ).arg( dataSourceUri() ), tr( "OGR" ) ); pushError( tr( "Cannot reopen datasource %1 in update mode" ).arg( dataSourceUri() ) ); return false; } } ++mUpdateModeStackDepth; // For implicitly entered updateMode, don't defer repacking mDeferRepack = !implicit; return true; } bool QgsOgrProvider::leaveUpdateMode() { if ( !mWriteAccessPossible ) { return false; } --mUpdateModeStackDepth; if ( mUpdateModeStackDepth < 0 ) { QgsMessageLog::logMessage( tr( "Unbalanced call to leaveUpdateMode() w.r.t. enterUpdateMode()" ), tr( "OGR" ) ); mUpdateModeStackDepth = 0; return false; } if ( mDeferRepack && mUpdateModeStackDepth == 0 ) { // Only repack once update mode is inactive if ( mShapefileMayBeCorrupted ) { repack(); } if ( mShapefileHadSpatialIndex ) { createSpatialIndexImpl(); } mShapefileMayBeCorrupted = false; mDeferRepack = false; } if ( !mDynamicWriteAccess ) { // The GeoJSON driver only properly flushes stuff in all situations by // closing and re-opening. Starting with GDAL 2.3.1, it should be safe to // use GDALDatasetFlush(). if ( mGDALDriverName == QLatin1String( "GeoJSON" ) ) { // Backup fields since if we created new fields, but didn't populate it // with any feature yet, it will disappear. const QgsFields oldFields = mAttributeFields; // This will also update mAttributeFields reloadData(); if ( mValid ) { // Make sure that new fields we added, but didn't populate yet, are // recreated at the OGR level, otherwise we won't be able to populate // them. for ( const auto &field : oldFields ) { int idx = mAttributeFields.lookupField( field.name() ); if ( idx < 0 ) { bool ignoreErrorOut = false; addAttributeOGRLevel( field, ignoreErrorOut ); mAttributeFields.append( field ); } } } } return true; } if ( mUpdateModeStackDepth == 0 ) { QgsDebugMsgLevel( QStringLiteral( "Reopening %1 in read-only mode" ).arg( dataSourceUri() ), 2 ); close(); open( OpenModeForceReadOnly ); if ( !mOgrLayer ) { QgsMessageLog::logMessage( tr( "Cannot reopen datasource %1 in read-only mode" ).arg( dataSourceUri() ), tr( "OGR" ) ); pushError( tr( "Cannot reopen datasource %1 in read-only mode" ).arg( dataSourceUri() ) ); return false; } } return true; } Qgis::ProviderStyleStorageCapabilities QgsOgrProvider::styleStorageCapabilities() const { Qgis::ProviderStyleStorageCapabilities storageCapabilities; if ( isValid() && ( mGDALDriverName == QLatin1String( "GPKG" ) || mGDALDriverName == QLatin1String( "SQLite" ) ) ) { storageCapabilities |= Qgis::ProviderStyleStorageCapability::SaveToDatabase; storageCapabilities |= Qgis::ProviderStyleStorageCapability::LoadFromDatabase; storageCapabilities |= Qgis::ProviderStyleStorageCapability::DeleteFromDatabase; } return storageCapabilities; } QString QgsOgrProvider::fileVectorFilters() const { return QgsOgrProviderUtils::fileVectorFilters(); } #undef TEXT_PROVIDER_KEY #undef TEXT_PROVIDER_DESCRIPTION ///@endcond