Skip to content

Commit

Permalink
[feature] Saving/loading SQL queries from Execute SQL/Update SQL dialogs
Browse files Browse the repository at this point in the history
This adds support for saving and loading SQL queries to a .sql text
file to the Execute SQL dialog and Update SQL dialogs.

Effectively, it ports this functionality from the DB Manager plugin
over to the core browser-based database connection facilities.

The UX has been designed to mimic the same functionality from
other standard parts of QGIS, eg the Processing Script Editor. Toolbar
actions are used accordingly, instead of the old text button approach
used in DB Manager.

Sponsored by City of Canning
  • Loading branch information
nyalldawson committed Feb 25, 2025
1 parent feb0dd7 commit 090e9fb
Show file tree
Hide file tree
Showing 7 changed files with 555 additions and 42 deletions.
75 changes: 75 additions & 0 deletions python/PyQt6/gui/auto_generated/qgsqueryresultwidget.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ be used in different contexts like when updating the SQL of an existing query la
#include "qgsqueryresultwidget.h"
%End
public:

enum class QueryWidgetMode /BaseType=IntFlag/
{
SqlQueryMode,
Expand Down Expand Up @@ -68,6 +69,7 @@ Sets the connection to ``connection``, ownership is transferred to the widget.
Convenience method to set the SQL editor text to ``sql``.
%End


public slots:

void notify( const QString &title, const QString &text, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
Expand Down Expand Up @@ -125,6 +127,79 @@ Emitted when the first batch of results has been fetched.
If the query returns no results this signal is not emitted.
%End


};

class QgsQueryResultDialog : QDialog
{
%Docstring(signature="appended")
A dialog which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).

.. note::

the ownership of the connection is transferred to the dialog.

.. seealso:: :py:class:`QgsQueryResultWidget`

.. versionadded:: 3.44
%End

%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, QWidget *parent = 0 );
%Docstring
Constructor for QgsQueryResultDialog.

Ownership of the ``connection`` is transferred to the dialog.
%End

QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the dialog.
%End

virtual void closeEvent( QCloseEvent *event );


};

class QgsQueryResultMainWindow : QMainWindow
{
%Docstring(signature="appended")
A main window which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).

.. note::

the ownership of the connection is transferred to the window.

.. seealso:: :py:class:`QgsQueryResultWidget`

.. versionadded:: 3.44
%End

%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, const QString &identifierName = QString() );
%Docstring
Constructor for QgsQueryResultMainWindow.

Ownership of the ``connection`` is transferred to the window.
%End

QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the window.
%End

virtual void closeEvent( QCloseEvent *event );


};

/************************************************************************
Expand Down
75 changes: 75 additions & 0 deletions python/gui/auto_generated/qgsqueryresultwidget.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ be used in different contexts like when updating the SQL of an existing query la
#include "qgsqueryresultwidget.h"
%End
public:

enum class QueryWidgetMode
{
SqlQueryMode,
Expand Down Expand Up @@ -68,6 +69,7 @@ Sets the connection to ``connection``, ownership is transferred to the widget.
Convenience method to set the SQL editor text to ``sql``.
%End


public slots:

void notify( const QString &title, const QString &text, Qgis::MessageLevel level = Qgis::MessageLevel::Info );
Expand Down Expand Up @@ -125,6 +127,79 @@ Emitted when the first batch of results has been fetched.
If the query returns no results this signal is not emitted.
%End


};

class QgsQueryResultDialog : QDialog
{
%Docstring(signature="appended")
A dialog which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).

.. note::

the ownership of the connection is transferred to the dialog.

.. seealso:: :py:class:`QgsQueryResultWidget`

.. versionadded:: 3.44
%End

%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultDialog( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, QWidget *parent = 0 );
%Docstring
Constructor for QgsQueryResultDialog.

Ownership of the ``connection`` is transferred to the dialog.
%End

QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the dialog.
%End

virtual void closeEvent( QCloseEvent *event );


};

class QgsQueryResultMainWindow : QMainWindow
{
%Docstring(signature="appended")
A main window which allows users to enter and run an SQL query on a
DB connection (an instance of :py:class:`QgsAbstractDatabaseProviderConnection`).

.. note::

the ownership of the connection is transferred to the window.

.. seealso:: :py:class:`QgsQueryResultWidget`

.. versionadded:: 3.44
%End

%TypeHeaderCode
#include "qgsqueryresultwidget.h"
%End
public:
QgsQueryResultMainWindow( QgsAbstractDatabaseProviderConnection *connection /Transfer/ = 0, const QString &identifierName = QString() );
%Docstring
Constructor for QgsQueryResultMainWindow.

Ownership of the ``connection`` is transferred to the window.
%End

QgsQueryResultWidget *resultWidget();
%Docstring
Returns the :py:class:`QgsQueryResultWidget` shown in the window.
%End

virtual void closeEvent( QCloseEvent *event );


};

/************************************************************************
Expand Down
18 changes: 3 additions & 15 deletions src/app/browser/qgsinbuiltdataitemproviders.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2010,25 +2010,13 @@ void QgsDatabaseItemGuiProvider::openSqlDialog( const QString &connectionUri, co

std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn( qgis::down_cast<QgsAbstractDatabaseProviderConnection *>( md->createConnection( connectionUri, QVariantMap() ) ) );

// Create the SQL dialog: this might become an independent class dialog in the future, for now
// we are still prototyping the features that this dialog will have.

QMainWindow *dialog = new QMainWindow();
dialog->setObjectName( QStringLiteral( "SQLCommandsDialog" ) );
if ( !identifierName.isEmpty() )
dialog->setWindowTitle( tr( "%1 — Execute SQL" ).arg( identifierName ) );
else
dialog->setWindowTitle( tr( "Execute SQL" ) );

QgsGui::enableAutoGeometryRestore( dialog );
QgsQueryResultMainWindow *dialog = new QgsQueryResultMainWindow( conn.release(), identifierName );
dialog->setAttribute( Qt::WA_DeleteOnClose );
dialog->setStyleSheet( QgisApp::instance()->styleSheet() );

QgsQueryResultWidget *widget { new QgsQueryResultWidget( nullptr, conn.release() ) };
widget->setQuery( query );
dialog->setCentralWidget( widget );
dialog->resultWidget()->setQuery( query );

connect( widget, &QgsQueryResultWidget::createSqlVectorLayer, widget, [provider, connectionUri, context]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
connect( dialog->resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, dialog, [provider, connectionUri, context]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( provider ) };
if ( !md )
return;
Expand Down
71 changes: 46 additions & 25 deletions src/app/qgsapplayertreeviewmenuprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -253,20 +253,32 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn2 )
{
QgsDialog dialog;
dialog.setObjectName( QStringLiteral( "SqlUpdateDialog" ) );
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layer->name() ) );
QgsGui::enableAutoGeometryRestore( &dialog );
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };
options.layerName = layer->name();
QgsQueryResultWidget *queryResultWidget { new QgsQueryResultWidget( &dialog, conn2.release() ) };
queryResultWidget->setWidgetMode( QgsQueryResultWidget::QueryWidgetMode::QueryLayerUpdateMode );
queryResultWidget->setSqlVectorLayerOptions( options );
queryResultWidget->executeQuery();
queryResultWidget->layout()->setContentsMargins( 0, 0, 0, 0 );
dialog.layout()->addWidget( queryResultWidget );

connect( queryResultWidget, &QgsQueryResultWidget::createSqlVectorLayer, queryResultWidget, [queryResultWidget, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {

QgsQueryResultDialog dialog( conn2.release() );
dialog.setObjectName( QStringLiteral( "SqlUpdateDialog" ) );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );

const QString layerName = layer->name();
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layerName ) );
QgsGui::enableAutoGeometryRestore( &dialog );
dialog.resultWidget()->setWidgetMode( QgsQueryResultWidget::QueryWidgetMode::QueryLayerUpdateMode );
dialog.resultWidget()->setSqlVectorLayerOptions( options );
dialog.resultWidget()->executeQuery();

connect( dialog.resultWidget(), &QgsQueryResultWidget::requestDialogTitleUpdate, &dialog, [&dialog, layerName]( const QString &fileName ) {
if ( fileName.isEmpty() )
{
dialog.setWindowTitle( tr( "%1 — Update SQL" ).arg( layerName ) );
}
else
{
dialog.setWindowTitle( tr( "%1 — %2 — Update SQL" ).arg( fileName, layerName ) );
}
} );

connect( dialog.resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, &dialog, [&dialog, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
( void ) this;
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn3 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn3 )
Expand All @@ -277,7 +289,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
if ( sqlLayer->isValid() )
{
layer->setDataSource( sqlLayer->source(), sqlLayer->name(), sqlLayer->dataProvider()->name(), QgsDataProvider::ProviderOptions() );
queryResultWidget->notify( QObject::tr( "Layer Update Success" ), QObject::tr( "The SQL layer was updated successfully" ), Qgis::MessageLevel::Success );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Success" ), QObject::tr( "The SQL layer was updated successfully" ), Qgis::MessageLevel::Success );
}
else
{
Expand All @@ -286,12 +298,12 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
{
error = QObject::tr( "layer is not valid, check the log messages for more information" );
}
queryResultWidget->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( error ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( error ), Qgis::MessageLevel::Critical );
}
}
catch ( QgsProviderConnectionException &ex )
{
queryResultWidget->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "Layer Update Error" ), QObject::tr( "Error updating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
}
}
} );
Expand All @@ -306,19 +318,28 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn2 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn2 )
{
QgsDialog dialog;
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };

QgsQueryResultDialog dialog( conn2.release() );
dialog.setObjectName( QStringLiteral( "SqlExecuteDialog" ) );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );
dialog.setWindowTitle( tr( "Execute SQL" ) );
QgsGui::enableAutoGeometryRestore( &dialog );
QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions options { conn2->sqlOptions( layer->source() ) };
QgsQueryResultWidget *queryResultWidget { new QgsQueryResultWidget( &dialog, conn2.release() ) };
queryResultWidget->setSqlVectorLayerOptions( options );
queryResultWidget->executeQuery();
queryResultWidget->layout()->setContentsMargins( 0, 0, 0, 0 );
dialog.layout()->addWidget( queryResultWidget );
dialog.setStyleSheet( QgisApp::instance()->styleSheet() );
dialog.resultWidget()->setSqlVectorLayerOptions( options );
dialog.resultWidget()->executeQuery();

connect( dialog.resultWidget(), &QgsQueryResultWidget::requestDialogTitleUpdate, &dialog, [&dialog]( const QString &fileName ) {
if ( fileName.isEmpty() )
{
dialog.setWindowTitle( tr( "Execute SQL" ) );
}
else
{
dialog.setWindowTitle( tr( "%1 — Execute SQL" ).arg( fileName ) );
}
} );

connect( queryResultWidget, &QgsQueryResultWidget::createSqlVectorLayer, queryResultWidget, [queryResultWidget, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
connect( dialog.resultWidget(), &QgsQueryResultWidget::createSqlVectorLayer, &dialog, [&dialog, layer, this]( const QString &, const QString &, const QgsAbstractDatabaseProviderConnection::SqlVectorLayerOptions &options ) {
( void ) this;
std::unique_ptr<QgsAbstractDatabaseProviderConnection> conn3 { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn3 )
Expand All @@ -330,7 +351,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
}
catch ( QgsProviderConnectionException &ex )
{
queryResultWidget->notify( QObject::tr( "New SQL Layer Creation Error" ), QObject::tr( "Error creating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
dialog.resultWidget()->notify( QObject::tr( "New SQL Layer Creation Error" ), QObject::tr( "Error creating the SQL layer: %1" ).arg( ex.what() ), Qgis::MessageLevel::Critical );
}
}
} );
Expand Down
Loading

0 comments on commit 090e9fb

Please sign in to comment.