diff --git a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp index 3a99458dadd..8d66138a19e 100644 --- a/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp +++ b/src/core/providers/ogr/qgsgeopackageproviderconnection.cpp @@ -23,8 +23,8 @@ // List of GPKG quoted system and dummy tables names to be excluded from the tables listing static const QStringList excludedTableNames { { QStringLiteral( "\"ogr_empty_table\"" ) } }; -QgsGeoPackageProviderConnection::QgsGeoPackageProviderConnection( const QString &name ): - QgsAbstractDatabaseProviderConnection( name ) +QgsGeoPackageProviderConnection::QgsGeoPackageProviderConnection( const QString &name ) + : QgsAbstractDatabaseProviderConnection( name ) { setDefaultCapabilities(); QgsSettings settings; diff --git a/src/providers/postgres/qgspostgresproviderconnection.cpp b/src/providers/postgres/qgspostgresproviderconnection.cpp index 018a043bf32..5bc337d0829 100644 --- a/src/providers/postgres/qgspostgresproviderconnection.cpp +++ b/src/providers/postgres/qgspostgresproviderconnection.cpp @@ -26,11 +26,11 @@ extern "C" #include } -QgsPostgresProviderConnection::QgsPostgresProviderConnection( const QString &name ): - QgsAbstractDatabaseProviderConnection( name ) +QgsPostgresProviderConnection::QgsPostgresProviderConnection( const QString &name ) + : QgsAbstractDatabaseProviderConnection( name ) { // Remove the sql and table empty parts - static const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=""\s*)raw" }; + const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=""\s*)raw" }; setUri( QgsPostgresConn::connUri( name ).uri().replace( removePartsRe, QString() ) ); setDefaultCapabilities(); } @@ -118,7 +118,7 @@ QString QgsPostgresProviderConnection::tableUri( const QString &schema, const QS dsUri.setSchema( schema ); if ( tableInfo.flags().testFlag( QgsAbstractDatabaseProviderConnection::TableFlag::Raster ) ) { - static const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=("[^"]+"\.?)*\s*)raw" }; + const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=("[^"]+"\.?)*\s*)raw" }; if ( tableInfo.geometryColumn().isEmpty() ) { throw QgsProviderConnectionException( QObject::tr( "Raster table '%1' in schema '%2' has no geometry column." ) diff --git a/src/providers/spatialite/CMakeLists.txt b/src/providers/spatialite/CMakeLists.txt index b5c9b2f02eb..3a581a3a0eb 100644 --- a/src/providers/spatialite/CMakeLists.txt +++ b/src/providers/spatialite/CMakeLists.txt @@ -12,6 +12,7 @@ SET(SPATIALITE_SRCS qgsspatialiteconnpool.cpp qgsspatialitefeatureiterator.cpp qgsspatialitetablemodel.cpp + qgsspatialiteproviderconnection.cpp ) SET(SPATIALITE_MOC_HDRS @@ -49,7 +50,9 @@ ENDIF () INCLUDE_DIRECTORIES( ${CMAKE_SOURCE_DIR}/external ${CMAKE_SOURCE_DIR}/src/core + ${CMAKE_SOURCE_DIR}/src/core/providers/ogr ${CMAKE_SOURCE_DIR}/src/core/expression + ${CMAKE_SOURCE_DIR}/src/core/symbology ${CMAKE_SOURCE_DIR}/src/core/geometry ${CMAKE_SOURCE_DIR}/src/core/metadata ${CMAKE_SOURCE_DIR}/src/gui @@ -61,6 +64,7 @@ INCLUDE_DIRECTORIES( ${CMAKE_BINARY_DIR}/src/ui ) INCLUDE_DIRECTORIES(SYSTEM + ${GDAL_INCLUDE_DIR} ${SQLITE3_INCLUDE_DIR} ${SPATIALITE_INCLUDE_DIR} ) diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index 96cfd75a76b..a4f6aac6ca1 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -30,6 +30,8 @@ email : a.furieri@lqt.it #include "qgsspatialitefeatureiterator.h" #include "qgsfeedback.h" #include "qgsspatialitedataitems.h" +#include "qgsspatialiteconnection.h" +#include "qgsspatialiteproviderconnection.h" #include "qgsjsonutils.h" #include "qgsvectorlayer.h" @@ -6054,6 +6056,34 @@ QList< QgsDataItemProvider * > QgsSpatiaLiteProviderMetadata::dataItemProviders( return providers; } + + +QMap QgsSpatiaLiteProviderMetadata::connections( bool cached ) +{ + return connectionsProtected< QgsSpatiaLiteProviderConnection, QgsSpatiaLiteConnection>( cached ); +} + +QgsAbstractProviderConnection *QgsSpatiaLiteProviderMetadata::createConnection( const QString &connName ) +{ + return new QgsSpatiaLiteProviderConnection( connName ); +} + +QgsAbstractProviderConnection *QgsSpatiaLiteProviderMetadata::createConnection( const QString &uri, const QVariantMap &configuration ) +{ + return new QgsSpatiaLiteProviderConnection( uri, configuration ); +} + +void QgsSpatiaLiteProviderMetadata::deleteConnection( const QString &name ) +{ + deleteConnectionProtected( name ); +} + +void QgsSpatiaLiteProviderMetadata::saveConnection( const QgsAbstractProviderConnection *conn, const QString &name ) +{ + saveConnectionProtected( conn, name ); +} + + QGISEXTERN QgsProviderMetadata *providerMetadataFactory() { return new QgsSpatiaLiteProviderMetadata(); diff --git a/src/providers/spatialite/qgsspatialiteprovider.h b/src/providers/spatialite/qgsspatialiteprovider.h index 9021b9fefcd..38cd5ffec5b 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.h +++ b/src/providers/spatialite/qgsspatialiteprovider.h @@ -406,6 +406,19 @@ class QgsSpatiaLiteProviderMetadata: public QgsProviderMetadata const QMap *options ) override; bool createDb( const QString &dbPath, QString &errCause ) override; QList< QgsDataItemProvider * > dataItemProviders() const override; + + // QgsProviderMetadata interface + public: + QMap connections( bool cached ) override; + QgsAbstractProviderConnection *createConnection( const QString &name ) override; + void deleteConnection( const QString &name ) override; + void saveConnection( const QgsAbstractProviderConnection *connection, const QString &name ) override; + + protected: + + QgsAbstractProviderConnection *createConnection( const QString &uri, const QVariantMap &configuration ) override; + + }; // clazy:excludeall=qstring-allocations diff --git a/src/providers/spatialite/qgsspatialiteproviderconnection.cpp b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp new file mode 100644 index 00000000000..a193b68bfde --- /dev/null +++ b/src/providers/spatialite/qgsspatialiteproviderconnection.cpp @@ -0,0 +1,395 @@ +/*************************************************************************** + QgsSpatialiteProviderConnection.cpp - QgsSpatialiteProviderConnection + + --------------------- + begin : 6.8.2019 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgsspatialiteproviderconnection.h" +#include "qgsspatialiteconnection.h" +#include "qgsspatialiteprovider.h" +#include "qgsogrprovider.h" +#include "qgssettings.h" +#include "qgsmessagelog.h" +#include "qgsproviderregistry.h" + + +QgsSpatiaLiteProviderConnection::QgsSpatiaLiteProviderConnection( const QString &name ) + : QgsAbstractDatabaseProviderConnection( name ) +{ + setDefaultCapabilities(); + // TODO: QGIS 4: move into QgsSettings::Section::Providers group + QgsSettings settings; + settings.beginGroup( QStringLiteral( "SpatiaLite" ) ); + settings.beginGroup( QStringLiteral( "connections" ) ); + settings.beginGroup( name ); + QgsDataSourceUri dsUri; + dsUri.setDatabase( settings.value( QStringLiteral( "sqlitepath" ) ).toString() ); + setUri( dsUri.uri() ); +} + +QgsSpatiaLiteProviderConnection::QgsSpatiaLiteProviderConnection( const QString &uri, const QVariantMap &configuration ): + QgsAbstractDatabaseProviderConnection( uri, configuration ) +{ + const QRegularExpression removePartsRe { R"raw(\s*sql=\s*|\s*table=""\s*|\([^\)]+\))raw" }; + // Cleanup the URI in case it contains other information other than the file path + setUri( QString( uri ).replace( removePartsRe, QString() ) ); + setDefaultCapabilities(); +} + +void QgsSpatiaLiteProviderConnection::store( const QString &name ) const +{ + // TODO: QGIS 4: move into QgsSettings::Section::Providers group + QgsSettings settings; + settings.beginGroup( QStringLiteral( "SpatiaLite" ) ); + settings.beginGroup( QStringLiteral( "connections" ) ); + settings.beginGroup( name ); + settings.setValue( QStringLiteral( "sqlitepath" ), pathFromUri() ); +} + +void QgsSpatiaLiteProviderConnection::remove( const QString &name ) const +{ + // TODO: QGIS 4: move into QgsSettings::Section::Providers group + QgsSettings settings; + settings.beginGroup( QStringLiteral( "SpatiaLite" ) ); + settings.beginGroup( QStringLiteral( "connections" ) ); + settings.remove( name ); +} + +QString QgsSpatiaLiteProviderConnection::tableUri( const QString &schema, const QString &name ) const +{ + const auto tableInfo { table( schema, name ) }; + return uri() + QStringLiteral( " table=%1" ).arg( QgsSqliteUtils::quotedIdentifier( name ) ); +} + +void QgsSpatiaLiteProviderConnection::createVectorTable( const QString &schema, + const QString &name, + const QgsFields &fields, + QgsWkbTypes::Type wkbType, + const QgsCoordinateReferenceSystem &srs, + bool overwrite, + const QMap *options ) const +{ + checkCapability( Capability::CreateVectorTable ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + QMap opts { *options }; + opts[ QStringLiteral( "layerName" ) ] = QVariant( name ); + opts[ QStringLiteral( "update" ) ] = true; + QMap map; + QString errCause; + QgsVectorLayerExporter::ExportError errCode = QgsSpatiaLiteProvider::createEmptyLayer( + uri() + QStringLiteral( " table=%1 (geom)" ).arg( QgsSqliteUtils::quotedIdentifier( name ) ), + fields, + wkbType, + srs, + overwrite, + &map, + &errCause, + &opts + ); + if ( errCode != QgsVectorLayerExporter::ExportError::NoError ) + { + throw QgsProviderConnectionException( QObject::tr( "An error occurred while creating the vector layer: %1" ).arg( errCause ) ); + } +} + +void QgsSpatiaLiteProviderConnection::dropVectorTable( const QString &schema, const QString &name ) const +{ + checkCapability( Capability::DropVectorTable ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + QString errCause; + + QgsSqliteHandle *hndl = QgsSqliteHandle::openDb( pathFromUri() ); + if ( !hndl ) + { + errCause = QObject::tr( "Connection to database failed" ); + } + + if ( errCause.isEmpty() ) + { + sqlite3 *sqlite_handle = hndl->handle(); + int ret; + if ( !gaiaDropTable( sqlite_handle, name.toUtf8().constData() ) ) + { + // unexpected error + errCause = QObject::tr( "Unable to delete table %1\n" ).arg( name ); + QgsSqliteHandle::closeDb( hndl ); + } + else + { + // TODO: remove spatial indexes? + // run VACUUM to free unused space and compact the database + ret = sqlite3_exec( sqlite_handle, "VACUUM", nullptr, nullptr, nullptr ); + if ( ret != SQLITE_OK ) + { + QgsDebugMsg( QStringLiteral( "Failed to run VACUUM after deleting table on database %1" ) + .arg( pathFromUri() ) ); + } + + QgsSqliteHandle::closeDb( hndl ); + } + } + if ( ! errCause.isEmpty() ) + { + throw QgsProviderConnectionException( QObject::tr( "Error deleting vector/aspatial table %1: %2" ).arg( name ).arg( errCause ) ); + } +} + + +void QgsSpatiaLiteProviderConnection::renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const +{ + checkCapability( Capability::RenameVectorTable ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + // TODO: maybe an index? + QString sql( QStringLiteral( "ALTER TABLE %1 RENAME TO %2" ) + .arg( QgsSqliteUtils::quotedIdentifier( name ), + QgsSqliteUtils::quotedIdentifier( newName ) ) ); + executeSqlPrivate( sql ); + sql = QStringLiteral( "UPDATE geometry_columns SET f_table_name = lower(%2) WHERE lower(f_table_name) = lower(%1)" ) + .arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( newName ) ); + executeSqlPrivate( sql ); + sql = QStringLiteral( "UPDATE layer_styles SET f_table_name = lower(%2) WHERE f_table_name = lower(%1)" ) + .arg( QgsSqliteUtils::quotedString( name ), + QgsSqliteUtils::quotedString( newName ) ); + try + { + executeSqlPrivate( sql ); + } + catch ( QgsProviderConnectionException &ex ) + { + QgsDebugMsgLevel( QStringLiteral( "Warning: error while updating the styles, perhaps there are no styles stored in this GPKG: %1" ).arg( ex.what() ), 4 ); + } +} + +QList> QgsSpatiaLiteProviderConnection::executeSql( const QString &sql ) const +{ + checkCapability( Capability::ExecuteSql ); + return executeSqlPrivate( sql ); +} + +void QgsSpatiaLiteProviderConnection::vacuum( const QString &schema, const QString &name ) const +{ + Q_UNUSED( name ) + checkCapability( Capability::Vacuum ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + executeSqlPrivate( QStringLiteral( "VACUUM" ) ); +} + + +QList QgsSpatiaLiteProviderConnection::tables( const QString &schema, const TableFlags &flags ) const +{ + checkCapability( Capability::Tables ); + if ( ! schema.isEmpty() ) + { + QgsMessageLog::logMessage( QStringLiteral( "Schema is not supported by Spatialite, ignoring" ), QStringLiteral( "OGR" ), Qgis::Info ); + } + QList tableInfo; + QString errCause; + QList results; + try + { + QgsSpatiaLiteConnection connection( pathFromUri() ); + QgsSpatiaLiteConnection::Error err = connection.fetchTables( true ); + if ( err != QgsSpatiaLiteConnection::NoError ) + { + QString msg; + switch ( err ) + { + case QgsSpatiaLiteConnection::NotExists: + msg = QObject::tr( "Database does not exist" ); + break; + case QgsSpatiaLiteConnection::FailedToOpen: + msg = QObject::tr( "Failed to open database" ); + break; + case QgsSpatiaLiteConnection::FailedToCheckMetadata: + msg = QObject::tr( "Failed to check metadata" ); + break; + case QgsSpatiaLiteConnection::FailedToGetTables: + msg = QObject::tr( "Failed to get list of tables" ); + break; + default: + msg = QObject::tr( "Unknown error" ); + break; + } + QString msgDetails = connection.errorMessage(); + if ( !msgDetails.isEmpty() ) + { + msg = QStringLiteral( "%1 (%2)" ).arg( msg, msgDetails ); + } + throw QgsProviderConnectionException( QObject::tr( "Error fetching table information for connection: %1" ).arg( pathFromUri() ) ); + } + else + { + + const QString connectionInfo = QStringLiteral( "dbname='%1'" ).arg( QString( connection.path() ).replace( '\'', QLatin1String( "\\'" ) ) ); + QgsDataSourceUri dsUri( connectionInfo ); + + // Need to store it here because provider (and underlying gaia library) returns views as spatial table if they have geometries + QStringList viewNames; + for ( const auto &tn : executeSqlPrivate( QStringLiteral( "SELECT name FROM sqlite_master WHERE type = 'view'" ) ) ) + { + viewNames.push_back( tn.first().toString() ); + } + + // Another wierdness: table names are converted to lowercase when out of spatialite gaia functions, let's get them back to their real case here, + // may need LAUNDER on open, but let's try to make it consistent with how GPKG works. + QgsStringMap tableNotLowercaseNames; + for ( const auto &tn : executeSqlPrivate( QStringLiteral( "SELECT name FROM sqlite_master WHERE LOWER(name) != name" ) ) ) + { + const QString tName { tn.first().toString() }; + tableNotLowercaseNames.insert( tName.toLower(), tName ); + } + + const auto constTables = connection.tables(); + for ( const QgsSpatiaLiteConnection::TableEntry &entry : constTables ) + { + QString tableName { tableNotLowercaseNames.value( entry.tableName, entry.tableName ) }; + dsUri.setDataSource( QString(), tableName, entry.column, QString(), QString() ); + QgsSpatiaLiteProviderConnection::TableProperty property; + property.setTableName( tableName ); + // Create a layer and get information from it + std::unique_ptr< QgsVectorLayer > vl = qgis::make_unique( dsUri.uri(), QString(), QLatin1Literal( "spatialite" ) ); + if ( vl->isValid() ) + { + if ( vl->isSpatial() ) + { + property.setGeometryColumnCount( 1 ); + property.setGeometryColumn( entry.column ); + property.setFlag( QgsSpatiaLiteProviderConnection::TableFlag::Vector ); + property.setGeometryColumnTypes( {{ vl->wkbType(), vl->crs() }} ); + } + else + { + property.setGeometryColumnCount( 0 ); + property.setGeometryColumnTypes( {{ QgsWkbTypes::NoGeometry, QgsCoordinateReferenceSystem() }} ); + property.setFlag( QgsSpatiaLiteProviderConnection::TableFlag::Aspatial ); + } + if ( viewNames.contains( tableName ) ) + { + property.setFlag( QgsSpatiaLiteProviderConnection::TableFlag::View ); + } + tableInfo.push_back( property ); + } + else + { + QgsDebugMsgLevel( QStringLiteral( "Layer is not valid: %1" ).arg( dsUri.uri() ), 2 ); + } + } + } + } + catch ( QgsProviderConnectionException &ex ) + { + errCause = ex.what(); + } + + if ( ! errCause.isEmpty() ) + { + throw QgsProviderConnectionException( QObject::tr( "Error listing tables from %1: %2" ).arg( pathFromUri() ).arg( errCause ) ); + } + // Filters + if ( flags ) + { + tableInfo.erase( std::remove_if( tableInfo.begin(), tableInfo.end(), [ & ]( const QgsAbstractDatabaseProviderConnection::TableProperty & ti ) + { + return !( ti.flags() & flags ); + } ), tableInfo.end() ); + } + return tableInfo ; +} + +void QgsSpatiaLiteProviderConnection::setDefaultCapabilities() +{ + mCapabilities = + { + Capability::Tables, + Capability::CreateVectorTable, + Capability::DropVectorTable, + Capability::RenameVectorTable, + Capability::Vacuum, + Capability::Spatial, + Capability::TableExists, + Capability::ExecuteSql, + }; +} + +QList QgsSpatiaLiteProviderConnection::executeSqlPrivate( const QString &sql ) const +{ + QString errCause; + QList results; + gdal::ogr_datasource_unique_ptr hDS( GDALOpenEx( pathFromUri().toUtf8().constData(), GDAL_OF_VECTOR | GDAL_OF_UPDATE, nullptr, nullptr, nullptr ) ); + if ( hDS ) + { + OGRLayerH ogrLayer( GDALDatasetExecuteSQL( hDS.get(), sql.toUtf8().constData(), nullptr, nullptr ) ); + if ( ogrLayer ) + { + gdal::ogr_feature_unique_ptr fet; + QgsFields fields; + while ( fet.reset( OGR_L_GetNextFeature( ogrLayer ) ), fet ) + { + QVariantList row; + // Try to get the right type for the returned values + if ( fields.isEmpty() ) + { + fields = QgsOgrUtils::readOgrFields( fet.get(), QTextCodec::codecForName( "UTF-8" ) ); + } + if ( ! fields.isEmpty() ) + { + QgsFeature f { QgsOgrUtils::readOgrFeature( fet.get(), fields, QTextCodec::codecForName( "UTF-8" ) ) }; + const QgsAttributes &constAttrs { f.attributes() }; + for ( int i = 0; i < constAttrs.length(); i++ ) + { + row.push_back( constAttrs.at( i ) ); + } + } + else // Fallback to strings + { + for ( int i = 0; i < OGR_F_GetFieldCount( fet.get() ); i++ ) + { + row.push_back( QVariant( QString::fromUtf8( OGR_F_GetFieldAsString( fet.get(), i ) ) ) ); + } + } + + results.push_back( row ); + } + GDALDatasetReleaseResultSet( hDS.get(), ogrLayer ); + } + errCause = CPLGetLastErrorMsg( ); + } + else + { + errCause = QObject::tr( "There was an error opening Spatialite %1!" ).arg( pathFromUri() ); + } + if ( ! errCause.isEmpty() ) + { + throw QgsProviderConnectionException( QObject::tr( "Error executing SQL %1: %2" ).arg( sql ).arg( errCause ) ); + } + return results; +} + +QString QgsSpatiaLiteProviderConnection::pathFromUri() const +{ + const QgsDataSourceUri dsUri( uri() ); + return dsUri.database(); +} + diff --git a/src/providers/spatialite/qgsspatialiteproviderconnection.h b/src/providers/spatialite/qgsspatialiteproviderconnection.h new file mode 100644 index 00000000000..3aa6b9eb353 --- /dev/null +++ b/src/providers/spatialite/qgsspatialiteproviderconnection.h @@ -0,0 +1,58 @@ +/*************************************************************************** + QgsSpatialiteProviderConnection.h - QgsSpatialiteProviderConnection + + --------------------- + begin : 6.8.2019 + copyright : (C) 2019 by Alessandro Pasotti + email : elpaso at itopen dot it + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSSPATIALITEPROVIDERCONNECTION_H +#define QGSSPATIALITEPROVIDERCONNECTION_H + +#include "qgsabstractdatabaseproviderconnection.h" + +///@cond PRIVATE +#define SIP_NO_FILE + +class QgsSpatiaLiteProviderConnection : public QgsAbstractDatabaseProviderConnection +{ + public: + + QgsSpatiaLiteProviderConnection( const QString &name ); + // Note: URI must be in PG QgsDataSourceUri format ( "dbname='path_to_sqlite.db'" ) + QgsSpatiaLiteProviderConnection( const QString &uri, const QVariantMap &configuration ); + + + // QgsAbstractProviderConnection interface + public: + void store( const QString &name ) const override; + void remove( const QString &name ) const override; + QString tableUri( const QString &schema, const QString &name ) const override; + void createVectorTable( const QString &schema, const QString &name, const QgsFields &fields, QgsWkbTypes::Type wkbType, const QgsCoordinateReferenceSystem &srs, bool overwrite, const QMap *options ) const override; + void dropVectorTable( const QString &schema, const QString &name ) const override; + void renameVectorTable( const QString &schema, const QString &name, const QString &newName ) const override; + QList> executeSql( const QString &sql ) const override; + void vacuum( const QString &schema, const QString &name ) const override; + QList tables( const QString &schema = QString(), + const TableFlags &flags = nullptr ) const override; + + private: + + void setDefaultCapabilities(); + //! Use GDAL to execute SQL + QList executeSqlPrivate( const QString &sql ) const; + + //! extract the path from the DS URI (which is in "PG" form: 'dbname=\'/path_to.sqlite\' table="table_name" (geom_col_name)') + QString pathFromUri() const; + +}; + +///@endcond +#endif // QGSSPATIALITEPROVIDERCONNECTION_H diff --git a/tests/src/python/test_qgsproviderconnection_base.py b/tests/src/python/test_qgsproviderconnection_base.py index 5314d833ba6..86cc8055f01 100644 --- a/tests/src/python/test_qgsproviderconnection_base.py +++ b/tests/src/python/test_qgsproviderconnection_base.py @@ -176,8 +176,11 @@ class TestPyQgsProviderConnectionBase(): self.assertEqual(res, []) sql = "SELECT string, long, double, integer, date, datetime FROM %s" % table res = conn.executeSql(sql) - # GPKG has no type for time - self.assertEqual(res, [['QGIS Rocks - \U0001f604', 666, 1.234, 1234, QtCore.QDate(2019, 7, 8), QtCore.QDateTime(2019, 7, 8, 12, 0, 12)]]) + # GPKG has no type for time and spatialite has no support for dates and time ... + if self.providerKey == 'spatialite': + self.assertEqual(res, [['QGIS Rocks - \U0001f604', 666, 1.234, 1234, '2019-07-08', '2019-07-08T12:00:12']]) + else: + self.assertEqual(res, [['QGIS Rocks - \U0001f604', 666, 1.234, 1234, QtCore.QDate(2019, 7, 8), QtCore.QDateTime(2019, 7, 8, 12, 0, 12)]]) sql = "SELECT time FROM %s" % table res = conn.executeSql(sql) self.assertIn(res, ([[QtCore.QTime(12, 0, 13)]], [['12:00:13.00']])) @@ -193,14 +196,15 @@ class TestPyQgsProviderConnectionBase(): self.assertTrue('myNewTable' in table_names) self.assertFalse('myNewAspatialTable' in table_names) - # Query for rasters (in qgis_test schema or no schema for GPKG) - table_properties = conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.Raster) - # At leasy one raster should be there - self.assertTrue(len(table_properties) >= 1) - table_property = table_properties[0] - self.assertTrue(table_property.flags() & QgsAbstractDatabaseProviderConnection.Raster) - self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Vector) - self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Aspatial) + # Query for rasters (in qgis_test schema or no schema for GPKG, spatialite has no support) + if self.providerKey != 'spatialite': + table_properties = conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.Raster) + # At least one raster should be there (except for spatialite) + self.assertTrue(len(table_properties) >= 1) + table_property = table_properties[0] + self.assertTrue(table_property.flags() & QgsAbstractDatabaseProviderConnection.Raster) + self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Vector) + self.assertFalse(table_property.flags() & QgsAbstractDatabaseProviderConnection.Aspatial) # Rename conn.renameVectorTable(schema, 'myNewTable', 'myVeryNewTable') diff --git a/tests/src/python/test_qgsproviderconnection_ogr_gpkg.py b/tests/src/python/test_qgsproviderconnection_ogr_gpkg.py index d820d72e65a..a6da05887b2 100644 --- a/tests/src/python/test_qgsproviderconnection_ogr_gpkg.py +++ b/tests/src/python/test_qgsproviderconnection_ogr_gpkg.py @@ -103,17 +103,17 @@ class TestPyQgsProviderConnectionGpkg(unittest.TestCase, TestPyQgsProviderConnec conn.createVectorTable('', 'myNewTable', QgsFields(), typ, crs, True, {}) # Check filters and special cases - table_names = self._table_names(conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.Raster)) + table_names = self._table_names(conn.tables('', QgsAbstractDatabaseProviderConnection.Raster)) self.assertTrue('osm' in table_names) self.assertFalse('myNewTable' in table_names) self.assertFalse('myNewAspatialTable' in table_names) - table_names = self._table_names(conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.View)) + table_names = self._table_names(conn.tables('', QgsAbstractDatabaseProviderConnection.View)) self.assertFalse('osm' in table_names) self.assertFalse('myNewTable' in table_names) self.assertFalse('myNewAspatialTable' in table_names) - table_names = self._table_names(conn.tables('qgis_test', QgsAbstractDatabaseProviderConnection.Aspatial)) + table_names = self._table_names(conn.tables('', QgsAbstractDatabaseProviderConnection.Aspatial)) self.assertFalse('osm' in table_names) self.assertFalse('myNewTable' in table_names) self.assertTrue('myNewAspatialTable' in table_names) diff --git a/tests/src/python/test_qgsproviderconnection_spatialite.py b/tests/src/python/test_qgsproviderconnection_spatialite.py new file mode 100644 index 00000000000..ccc5405c5b3 --- /dev/null +++ b/tests/src/python/test_qgsproviderconnection_spatialite.py @@ -0,0 +1,113 @@ +# -*- coding: utf-8 -*- +"""QGIS Unit tests for Spatialite QgsAbastractProviderConnection API. + +.. note:: 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. + +""" +__author__ = 'Alessandro Pasotti' +__date__ = '28/10/2019' +__copyright__ = 'Copyright 2019, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + +import os +import shutil +from test_qgsproviderconnection_base import TestPyQgsProviderConnectionBase +from qgis.core import ( + QgsWkbTypes, + QgsAbstractDatabaseProviderConnection, + QgsProviderConnectionException, + QgsVectorLayer, + QgsRasterLayer, + QgsProviderRegistry, + QgsFields, + QgsCoordinateReferenceSystem, +) +from qgis.testing import unittest +from utilities import unitTestDataPath + +TEST_DATA_DIR = unitTestDataPath() + + +class TestPyQgsProviderConnectionSpatialite(unittest.TestCase, TestPyQgsProviderConnectionBase): + + # Provider test cases must define the string URI for the test + uri = '' + # Provider test cases must define the provider name (e.g. "postgres" or "ogr") + providerKey = 'spatialite' + + @classmethod + def setUpClass(cls): + """Run before all tests""" + TestPyQgsProviderConnectionBase.setUpClass() + spatialite_original_path = '{}/qgis_server/test_project_wms_grouped_layers.sqlite'.format(TEST_DATA_DIR) + cls.spatialite_path = '{}/qgis_server/test_project_wms_grouped_layers_test.sqlite'.format(TEST_DATA_DIR) + shutil.copy(spatialite_original_path, cls.spatialite_path) + cls.uri = "dbname=\'%s\'" % cls.spatialite_path + vl = QgsVectorLayer('{} table=\'cdb_lines\''.format(cls.uri), 'test', 'spatialite') + assert vl.isValid() + + @classmethod + def tearDownClass(cls): + """Run after all tests""" + os.unlink(cls.spatialite_path) + + def test_spatialite_connections_from_uri(self): + """Create a connection from a layer uri and retrieve it""" + + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + vl = QgsVectorLayer('{} table=\'cdb_lines\''.format(self.uri), 'test', 'spatialite') + self.assertTrue(vl.isValid()) + conn = md.createConnection(vl.dataProvider().uri().uri(), {}) + self.assertEqual(conn.uri(), self.uri + ' table="cdb_lines"') + conn.tables() + + def test_spatialite_table_uri(self): + """Create a connection from a layer uri and create a table URI""" + + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + conn = md.createConnection(self.uri, {}) + self.assertEqual(conn.tableUri('', 'cdb_lines'), '{} table="cdb_lines"'.format(self.uri)) + vl = QgsVectorLayer(conn.tableUri('', 'cdb_lines'), 'lines', 'spatialite') + self.assertTrue(vl.isValid()) + + # Test table(), throws if not found + table_info = conn.table('', 'cdb_lines') + + def test_spatialite_connections(self): + """Create some connections and retrieve them""" + + md = QgsProviderRegistry.instance().providerMetadata('spatialite') + + conn = md.createConnection(self.uri, {}) + md.saveConnection(conn, 'qgis_test1') + + # Retrieve capabilities + capabilities = conn.capabilities() + self.assertTrue(bool(capabilities & QgsAbstractDatabaseProviderConnection.Tables)) + self.assertFalse(bool(capabilities & QgsAbstractDatabaseProviderConnection.Schemas)) + self.assertTrue(bool(capabilities & QgsAbstractDatabaseProviderConnection.CreateVectorTable)) + self.assertTrue(bool(capabilities & QgsAbstractDatabaseProviderConnection.DropVectorTable)) + self.assertTrue(bool(capabilities & QgsAbstractDatabaseProviderConnection.RenameVectorTable)) + self.assertFalse(bool(capabilities & QgsAbstractDatabaseProviderConnection.RenameRasterTable)) + + crs = QgsCoordinateReferenceSystem.fromEpsgId(3857) + typ = QgsWkbTypes.LineString + conn.createVectorTable('', 'myNewAspatialTable', QgsFields(), QgsWkbTypes.NoGeometry, crs, True, {}) + conn.createVectorTable('', 'myNewTable', QgsFields(), typ, crs, True, {}) + + table_names = self._table_names(conn.tables('', QgsAbstractDatabaseProviderConnection.View)) + self.assertTrue('my_view' in table_names) + self.assertFalse('myNewTable' in table_names) + self.assertFalse('myNewAspatialTable' in table_names) + + table_names = self._table_names(conn.tables('', QgsAbstractDatabaseProviderConnection.Aspatial)) + self.assertFalse('myNewTable' in table_names) + self.assertTrue('myNewAspatialTable' in table_names) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/testdata/qgis_server/test_project_wms_grouped_layers.sqlite b/tests/testdata/qgis_server/test_project_wms_grouped_layers.sqlite new file mode 100644 index 00000000000..c4b00bb2a07 Binary files /dev/null and b/tests/testdata/qgis_server/test_project_wms_grouped_layers.sqlite differ