Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GPKG Allow table creation from file browser #41355

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions python/core/auto_generated/qgsdataitem.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -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/;
nyalldawson marked this conversation as resolved.
Show resolved Hide resolved
%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:
Expand Down Expand Up @@ -629,6 +640,7 @@ Returns icon for point cloud layer

:return: the layer name
%End

};


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

virtual QgsAbstractDatabaseProviderConnection *databaseConnection() const;


protected:

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

.. versionadded:: 3.4
%End

};


Expand Down Expand Up @@ -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
Expand Down
161 changes: 81 additions & 80 deletions src/app/browser/qgsinbuiltdataitemproviders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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 );
}
}
}
}


44 changes: 44 additions & 0 deletions src/core/providers/ogr/qgsogrdataitems.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
3 changes: 3 additions & 0 deletions src/core/providers/ogr/qgsogrdataitems.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
64 changes: 64 additions & 0 deletions src/core/qgsdataitem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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" ) );
Expand Down Expand Up @@ -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 )
Expand Down Expand Up @@ -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 )
Expand Down
Loading