Skip to content
Permalink
Browse files

Merge pull request #41355 from elpaso/bugfix-gh41342-gpkg-create-tabl…

…e-from-file-browser

GPKG Allow table creation from file browser
  • Loading branch information
nyalldawson committed Feb 8, 2021
2 parents 92fc1a7 + 1042136 commit 8e5736813332e3ade1ab212619880a0ddf8bee8b
@@ -404,6 +404,17 @@ Sets a custom sorting ``key`` for the item.
void moveToThread( QThread *targetThread );
%Docstring
Move object and all its descendants to thread
%End

virtual QgsAbstractDatabaseProviderConnection *databaseConnection() const /Factory/;
%Docstring
For data items that represent a DB connection or one of its children,
this method returns a connection.
All other data items will return NULL.

Ownership of the returned objects is transferred to the caller.

.. versionadded:: 3.16
%End

protected:
@@ -629,6 +640,7 @@ Returns icon for point cloud layer

:return: the layer name
%End

};


@@ -674,6 +686,9 @@ Returns the standard browser data collection icon.
.. seealso:: :py:func:`iconDir`
%End

virtual QgsAbstractDatabaseProviderConnection *databaseConnection() const;


protected:

static QIcon openDirIcon();
@@ -689,6 +704,7 @@ Shared home directory icon.

.. versionadded:: 3.4
%End

};


@@ -720,6 +736,8 @@ The optional ``providerKey`` string can be used to specify the key for the :py:c

~QgsDatabaseSchemaItem();

virtual QgsAbstractDatabaseProviderConnection *databaseConnection() const;


static QIcon iconDataCollection();
%Docstring
@@ -878,105 +878,106 @@ QString QgsDatabaseItemGuiProvider::name()
void QgsDatabaseItemGuiProvider::populateContextMenu( QgsDataItem *item, QMenu *menu, const QList<QgsDataItem *> &selectedItems, QgsDataItemGuiContext context )
{
Q_UNUSED( selectedItems )

// Add create new table for collection items but not not if it is a root item
if ( ! qobject_cast<QgsConnectionsRootItem *>( item ) )
{
if ( QgsDataCollectionItem * collectionItem { qobject_cast<QgsDataCollectionItem *>( item ) } )
{
// This is super messy: we need the QgsDataProvider key and NOT the QgsDataItemProvider key!
const QString dataProviderKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( collectionItem->providerKey() ) };
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) };
if ( md )
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn( item->databaseConnection() );

if ( conn && conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateVectorTable ) )
{
// Note: we could have used layerCollection() but casting to QgsDatabaseSchemaItem is more explicit
const bool isSchema { qobject_cast<QgsDatabaseSchemaItem *>( item ) != nullptr };
const QString connectionName { isSchema ? collectionItem->parent()->name() : collectionItem->name() };
// Not all data providers implement the connections API, let's try ...
try

QAction *newTableAction = new QAction( QObject::tr( "New Table…" ), menu );

QObject::connect( newTableAction, &QAction::triggered, collectionItem, [ collectionItem, context]
{
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn( static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionName ) ) );
if ( conn && conn->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateVectorTable ) )

std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2( collectionItem->databaseConnection() );
// This should never happen but let's play safe
if ( ! conn2 )
{
QgsMessageLog::logMessage( tr( "Connection to the database (%1) was lost." ).arg( collectionItem->name() ) );
return;
}

QgsNewVectorTableDialog dlg { conn2.get(), nullptr };
dlg.setCrs( QgsProject::instance()->defaultCrsForNewLayers() );

const bool isSchema { qobject_cast<QgsDatabaseSchemaItem *>( collectionItem ) != nullptr };

if ( isSchema )
{
dlg.setSchemaName( collectionItem->name() );
}

if ( dlg.exec() == QgsNewVectorTableDialog::DialogCode::Accepted )
{
QAction *newTableAction = new QAction( QObject::tr( "New Table…" ), menu );
QObject::connect( newTableAction, &QAction::triggered, collectionItem, [ collectionItem, connectionName, md, isSchema, context]
const QgsFields fields { dlg.fields() };
const QString tableName { dlg.tableName() };
const QString schemaName { dlg.schemaName() };
const QString geometryColumn { dlg.geometryColumnName() };
const QgsWkbTypes::Type geometryType { dlg.geometryType() };
const bool createSpatialIndex { dlg.createSpatialIndex() &&
geometryType != QgsWkbTypes::NoGeometry &&
geometryType != QgsWkbTypes::Unknown };
const QgsCoordinateReferenceSystem crs { dlg.crs( ) };
// This flag tells to the provider that field types do not need conversion
QMap<QString, QVariant> options { { QStringLiteral( "skipConvertFields" ), true } };

if ( ! geometryColumn.isEmpty() )
{
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionName ) ) };
QgsNewVectorTableDialog dlg { conn2.get(), nullptr };
dlg.setCrs( QgsProject::instance()->defaultCrsForNewLayers() );
if ( isSchema )
{
dlg.setSchemaName( collectionItem->name() );
}
if ( dlg.exec() == QgsNewVectorTableDialog::DialogCode::Accepted )
{
const QgsFields fields { dlg.fields() };
const QString tableName { dlg.tableName() };
const QString schemaName { dlg.schemaName() };
const QString geometryColumn { dlg.geometryColumnName() };
const QgsWkbTypes::Type geometryType { dlg.geometryType() };
const bool createSpatialIndex { dlg.createSpatialIndex() &&
geometryType != QgsWkbTypes::NoGeometry &&
geometryType != QgsWkbTypes::Unknown };
const QgsCoordinateReferenceSystem crs { dlg.crs( ) };
// This flag tells to the provider that field types do not need conversion
QMap<QString, QVariant> options { { QStringLiteral( "skipConvertFields" ), true } };

if ( ! geometryColumn.isEmpty() )
{
options[ QStringLiteral( "geometryColumn" ) ] = geometryColumn;
}
options[ QStringLiteral( "geometryColumn" ) ] = geometryColumn;
}

try
{
conn2->createVectorTable( schemaName, tableName, fields, geometryType, crs, true, &options );
if ( createSpatialIndex && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateSpatialIndex ) )
{
try
{
conn2->createVectorTable( schemaName, tableName, fields, geometryType, crs, true, &options );
if ( createSpatialIndex && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::CreateSpatialIndex ) )
{
try
{
conn2->createSpatialIndex( schemaName, tableName );
}
catch ( QgsProviderConnectionException &ex )
{
notify( QObject::tr( "Create Spatial Index" ), QObject::tr( "Could not create spatial index for table '%1':%2." ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Warning );
}
}
// Ok, here is the trick: we cannot refresh the connection item because the refresh is not
// recursive.
// So, we check if the item is a schema or not, if it's not it means we initiated the new table from
// the parent connection item, hence we search for the schema item and refresh it instead of refreshing
// the connection item (the parent) with no effects.
if ( ! isSchema && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::Schemas ) )
{
const auto constChildren { collectionItem->children() };
for ( const auto &c : constChildren )
{
if ( c->name() == schemaName )
{
c->refresh();
}
}
}
else
{
collectionItem->refresh( );
}
notify( QObject::tr( "New Table Created" ), QObject::tr( "Table '%1' was created successfully." ).arg( tableName ), context, Qgis::MessageLevel::Success );
conn2->createSpatialIndex( schemaName, tableName );
}
catch ( QgsProviderConnectionException &ex )
{
notify( QObject::tr( "New Table Creation Error" ), QObject::tr( "Error creating new table '%1': %2" ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Critical );
notify( QObject::tr( "Create Spatial Index" ), QObject::tr( "Could not create spatial index for table '%1':%2." ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Warning );
}
}
// Ok, here is the trick: we cannot refresh the connection item because the refresh is not
// recursive.
// So, we check if the item is a schema or not, if it's not it means we initiated the new table from
// the parent connection item, hence we search for the schema item and refresh it instead of refreshing
// the connection item (the parent) with no effects.
if ( ! isSchema && conn2->capabilities().testFlag( QgsAbstractDatabaseProviderConnection::Capability::Schemas ) )
{
const auto constChildren { collectionItem->children() };
for ( const auto &c : constChildren )
{
if ( c->name() == schemaName )
{
c->refresh();
}
}

}
} );
menu->addAction( newTableAction );
else
{
collectionItem->refresh( );
}
notify( QObject::tr( "New Table Created" ), QObject::tr( "Table '%1' was created successfully." ).arg( tableName ), context, Qgis::MessageLevel::Success );
}
catch ( QgsProviderConnectionException &ex )
{
notify( QObject::tr( "New Table Creation Error" ), QObject::tr( "Error creating new table '%1': %2" ).arg( tableName, ex.what() ), context, Qgis::MessageLevel::Critical );
}

}
}
catch ( QgsProviderConnectionException &ex )
{
// This is expected and it is not an error in case the provider does not implement the connections API
}
} );
menu->addAction( newTableAction );
}
}
}
}


@@ -461,6 +461,50 @@ QgsMimeDataUtils::Uri QgsOgrDataCollectionItem::mimeUri() const
return u;
}

QgsAbstractDatabaseProviderConnection *QgsOgrDataCollectionItem::databaseConnection() const
{

QgsAbstractDatabaseProviderConnection *conn { QgsDataCollectionItem::databaseConnection() };

// There is a chance that this is a spatialite file, but spatialite is not handled by OGR and
// it's not even in core.
if ( ! conn )
{

// test that file is valid with OGR
if ( OGRGetDriverCount() == 0 )
{
OGRRegisterAll();
}
// do not print errors, but write to debug
CPLPushErrorHandler( CPLQuietErrorHandler );
CPLErrorReset();
gdal::dataset_unique_ptr hDS( GDALOpenEx( path().toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr ) );
CPLPopErrorHandler();

if ( ! hDS )
{
QgsDebugMsgLevel( QStringLiteral( "GDALOpen error # %1 : %2 on %3" ).arg( CPLGetLastErrorNo() ).arg( CPLGetLastErrorMsg() ).arg( path() ), 2 );
return nullptr;
}

GDALDriverH hDriver = GDALGetDatasetDriver( hDS.get() );
QString driverName = GDALGetDriverShortName( hDriver );

if ( driverName == QLatin1String( "SQLite" ) )
{
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "spatialite" ) ) };
if ( md )
{
QgsDataSourceUri uri;
uri.setDatabase( path( ) );
conn = static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( uri.uri(), {} ) );
}
}
}
return conn;
}

// ---------------------------------------------------------------------------

QString QgsOgrDataItemProvider::name()
@@ -132,6 +132,9 @@ class CORE_EXPORT QgsOgrDataCollectionItem final: public QgsDataCollectionItem

bool hasDragEnabled() const override;
QgsMimeDataUtils::Uri mimeUri() const override;

QgsAbstractDatabaseProviderConnection *databaseConnection() const override;

};

//! Provider for OGR root data item
@@ -131,6 +131,44 @@ QIcon QgsDataCollectionItem::homeDirIcon()
return QgsApplication::getThemeIcon( QStringLiteral( "mIconFolderHome.svg" ) );
}

QgsAbstractDatabaseProviderConnection *QgsDataCollectionItem::databaseConnection() const
{
const QString dataProviderKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( providerKey() ) };
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) };

if ( ! md )
{
return nullptr;
}

const QString connectionName { name() };

try
{
// First try to retrieve the connection by name if this is a stored connection
if ( md->findConnection( connectionName ) )
{
return static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionName ) );
}

// If that fails, try to create a connection from the path, in case this is a
// filesystem-based DB (gpkg or spatialite)
// The name is useless, we need to get the file path from the data item path
const QString databaseFilePath { path().remove( QRegularExpression( R"re([\aZ]{2,}://)re" ) ) };

if ( QFile::exists( databaseFilePath ) )
{
return static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( databaseFilePath, {} ) );
}
}
catch ( QgsProviderConnectionException &ex )
{
// This is expected and it is not an error in case the provider does not implement
// the connections API
}
return nullptr;
}

QIcon QgsDataCollectionItem::iconDir()
{
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconFolder.svg" ) );
@@ -423,6 +461,11 @@ void QgsDataItem::moveToThread( QThread *targetThread )
QObject::moveToThread( targetThread );
}

QgsAbstractDatabaseProviderConnection *QgsDataItem::databaseConnection() const
{
return nullptr;
}

QIcon QgsDataItem::icon()
{
if ( state() == Populating && sPopulatingIcon )
@@ -1848,6 +1891,27 @@ QIcon QgsDatabaseSchemaItem::iconDataCollection()
return QgsApplication::getThemeIcon( QStringLiteral( "/mIconDbSchema.svg" ) );
}

QgsAbstractDatabaseProviderConnection *QgsDatabaseSchemaItem::databaseConnection() const
{
const QString dataProviderKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( providerKey() ) };
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) };
if ( ! md )
{
return nullptr;
}
const QString connectionName { parent()->name() };
try
{
return static_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionName ) );
}
catch ( QgsProviderConnectionException &ex )
{
// This is expected and it is not an error in case the provider does not implement
// the connections API
}
return nullptr;
}


QgsConnectionsRootItem::QgsConnectionsRootItem( QgsDataItem *parent, const QString &name, const QString &path, const QString &providerKey )
: QgsDataCollectionItem( parent, name, path, providerKey )

0 comments on commit 8e57368

Please sign in to comment.