Create a class for managing stored database queries

This class is designed to be compatible with DB Manager's storage
of queries in projects, but extended to allow storage within
the local profile too.
This commit is contained in:
Nyall Dawson 2025-02-25 14:55:25 +10:00
parent c8fe793e53
commit bb4827e9f9
No known key found for this signature in database
GPG Key ID: 4C61673F0BF197FC
23 changed files with 1007 additions and 1 deletions

View File

@ -6187,6 +6187,19 @@ Qgis.HistoryProviderBackend.baseClass = Qgis
Qgis.HistoryProviderBackends = lambda flags=0: Qgis.HistoryProviderBackend(flags)
Qgis.HistoryProviderBackends.baseClass = Qgis
HistoryProviderBackends = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.QueryStorageBackend.LocalProfile.__doc__ = "Local user profile"
Qgis.QueryStorageBackend.CurrentProject.__doc__ = "Current QGIS project"
Qgis.QueryStorageBackend.__doc__ = """Stored query storage backends.
.. versionadded:: 3.44
* ``LocalProfile``: Local user profile
* ``CurrentProject``: Current QGIS project
"""
# --
Qgis.QueryStorageBackend.baseClass = Qgis
QgsProcessing.SourceType = Qgis.ProcessingSourceType
# monkey patching scoped based enum
QgsProcessing.TypeMapLayer = Qgis.ProcessingSourceType.MapLayer

View File

@ -1932,6 +1932,12 @@ The development version
typedef QFlags<Qgis::HistoryProviderBackend> HistoryProviderBackends;
enum class QueryStorageBackend /BaseType=IntEnum/
{
LocalProfile,
CurrentProject,
};
enum class ProcessingSourceType /BaseType=IntEnum/
{
MapLayer,

View File

@ -45,6 +45,7 @@ try:
QgsGui.windowManager = staticmethod(QgsGui.windowManager)
QgsGui.setWindowManager = staticmethod(QgsGui.setWindowManager)
QgsGui.inputControllerManager = staticmethod(QgsGui.inputControllerManager)
QgsGui.storedQueryManager = staticmethod(QgsGui.storedQueryManager)
QgsGui.higFlags = staticmethod(QgsGui.higFlags)
QgsGui.sampleColor = staticmethod(QgsGui.sampleColor)
QgsGui.findScreenAt = staticmethod(QgsGui.findScreenAt)

View File

@ -0,0 +1,10 @@
# The following has been generated automatically from src/gui/qgsstoredquerymanager.h
try:
QgsStoredQueryManager.QueryDetails.__attribute_docs__ = {'name': 'Name of the query.', 'definition': 'Query definition.', 'backend': 'Storage backend.'}
except (NameError, AttributeError):
pass
try:
QgsStoredQueryManager.__attribute_docs__ = {'queryAdded': 'Emitted when a query is added to the manager.\n', 'queryChanged': 'Emitted when an existing query is changed in the manager.\n', 'queryRemoved': 'Emitted when a query is removed from the manager.\n'}
QgsStoredQueryManager.__signal_arguments__ = {'queryAdded': ['name: str', 'backend: Qgis.QueryStorageBackend'], 'queryChanged': ['name: str', 'backend: Qgis.QueryStorageBackend'], 'queryRemoved': ['name: str', 'backend: Qgis.QueryStorageBackend']}
except (NameError, AttributeError):
pass

View File

@ -215,6 +215,13 @@ Sets the global window ``manager``. Ownership is transferred to the QgsGui insta
Returns the global input controller manager.
.. versionadded:: 3.32
%End
static QgsStoredQueryManager *storedQueryManager() /KeepReference/;
%Docstring
Returns the global stored SQL query manager.
.. versionadded:: 3.44
%End
enum HigFlag /BaseType=IntEnum/

View File

@ -0,0 +1,121 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsstoredquerymanager.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
class QgsStoredQueryManager : QObject
{
%Docstring(signature="appended")
A manager for stored SQL queries.
:py:class:`QgsStoredQueryManager` is not usually directly created, instead
use the instance accessible through :py:func:`QgsGui.storedQueryManager()`.
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsstoredquerymanager.h"
%End
public:
QgsStoredQueryManager( QObject *parent = 0 );
%Docstring
Constructor for QgsStoredQueryManager, with the specified
``parent`` object.
%End
void storeQuery( const QString &name, const QString &query, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
%Docstring
Saves a query to the manager.
If a query with the same ``name`` already exists it will be overwritten with the new definition.
:param name: user-set, unique name for the query.
:param query: query definition to store
:param backend: storage backend for query
.. seealso:: :py:func:`queryAdded`
.. seealso:: :py:func:`queryChanged`
%End
void removeQuery( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
%Docstring
Removes the stored query with matching ``name``.
:param name: name of query to remove
:param backend: storage backend for query
.. seealso:: :py:func:`queryRemoved`
%End
QStringList allQueryNames( Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
%Docstring
Returns a list of the names of all stored queries for the specified ``backend``.
%End
QString query( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
%Docstring
Returns the query definition with matching ``name``, from the specified ``backend``.
%End
class QueryDetails
{
%Docstring(signature="appended")
Contains details about a stored query.
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsstoredquerymanager.h"
%End
public:
QString name;
QString definition;
Qgis::QueryStorageBackend backend;
};
QList< QgsStoredQueryManager::QueryDetails > allQueries() const;
%Docstring
Returns details of all queries stored in the manager.
Queries will be sorted by name.
%End
signals:
void queryAdded( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when a query is added to the manager.
%End
void queryChanged( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when an existing query is changed in the manager.
%End
void queryRemoved( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when a query is removed from the manager.
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsstoredquerymanager.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -208,6 +208,7 @@
%Include auto_generated/qgssourceselectprovider.sip
%Include auto_generated/qgssourceselectproviderregistry.sip
%Include auto_generated/qgsstatusbar.sip
%Include auto_generated/qgsstoredquerymanager.sip
%Include auto_generated/qgsstyleitemslistwidget.sip
%Include auto_generated/qgssublayersdialog.sip
%Include auto_generated/qgssubstitutionlistwidget.sip

View File

@ -6128,6 +6128,19 @@ Qgis.HistoryProviderBackend.__doc__ = """History provider backends.
Qgis.HistoryProviderBackend.baseClass = Qgis
Qgis.HistoryProviderBackends.baseClass = Qgis
HistoryProviderBackends = Qgis # dirty hack since SIP seems to introduce the flags in module
# monkey patching scoped based enum
Qgis.QueryStorageBackend.LocalProfile.__doc__ = "Local user profile"
Qgis.QueryStorageBackend.CurrentProject.__doc__ = "Current QGIS project"
Qgis.QueryStorageBackend.__doc__ = """Stored query storage backends.
.. versionadded:: 3.44
* ``LocalProfile``: Local user profile
* ``CurrentProject``: Current QGIS project
"""
# --
Qgis.QueryStorageBackend.baseClass = Qgis
QgsProcessing.SourceType = Qgis.ProcessingSourceType
# monkey patching scoped based enum
QgsProcessing.TypeMapLayer = Qgis.ProcessingSourceType.MapLayer

View File

@ -1932,6 +1932,12 @@ The development version
typedef QFlags<Qgis::HistoryProviderBackend> HistoryProviderBackends;
enum class QueryStorageBackend
{
LocalProfile,
CurrentProject,
};
enum class ProcessingSourceType
{
MapLayer,

View File

@ -30,6 +30,7 @@ try:
QgsGui.windowManager = staticmethod(QgsGui.windowManager)
QgsGui.setWindowManager = staticmethod(QgsGui.setWindowManager)
QgsGui.inputControllerManager = staticmethod(QgsGui.inputControllerManager)
QgsGui.storedQueryManager = staticmethod(QgsGui.storedQueryManager)
QgsGui.higFlags = staticmethod(QgsGui.higFlags)
QgsGui.sampleColor = staticmethod(QgsGui.sampleColor)
QgsGui.findScreenAt = staticmethod(QgsGui.findScreenAt)

View File

@ -0,0 +1,10 @@
# The following has been generated automatically from src/gui/qgsstoredquerymanager.h
try:
QgsStoredQueryManager.QueryDetails.__attribute_docs__ = {'name': 'Name of the query.', 'definition': 'Query definition.', 'backend': 'Storage backend.'}
except (NameError, AttributeError):
pass
try:
QgsStoredQueryManager.__attribute_docs__ = {'queryAdded': 'Emitted when a query is added to the manager.\n', 'queryChanged': 'Emitted when an existing query is changed in the manager.\n', 'queryRemoved': 'Emitted when a query is removed from the manager.\n'}
QgsStoredQueryManager.__signal_arguments__ = {'queryAdded': ['name: str', 'backend: Qgis.QueryStorageBackend'], 'queryChanged': ['name: str', 'backend: Qgis.QueryStorageBackend'], 'queryRemoved': ['name: str', 'backend: Qgis.QueryStorageBackend']}
except (NameError, AttributeError):
pass

View File

@ -215,6 +215,13 @@ Sets the global window ``manager``. Ownership is transferred to the QgsGui insta
Returns the global input controller manager.
.. versionadded:: 3.32
%End
static QgsStoredQueryManager *storedQueryManager() /KeepReference/;
%Docstring
Returns the global stored SQL query manager.
.. versionadded:: 3.44
%End
enum HigFlag

View File

@ -0,0 +1,121 @@
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsstoredquerymanager.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/
class QgsStoredQueryManager : QObject
{
%Docstring(signature="appended")
A manager for stored SQL queries.
:py:class:`QgsStoredQueryManager` is not usually directly created, instead
use the instance accessible through :py:func:`QgsGui.storedQueryManager()`.
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsstoredquerymanager.h"
%End
public:
QgsStoredQueryManager( QObject *parent = 0 );
%Docstring
Constructor for QgsStoredQueryManager, with the specified
``parent`` object.
%End
void storeQuery( const QString &name, const QString &query, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
%Docstring
Saves a query to the manager.
If a query with the same ``name`` already exists it will be overwritten with the new definition.
:param name: user-set, unique name for the query.
:param query: query definition to store
:param backend: storage backend for query
.. seealso:: :py:func:`queryAdded`
.. seealso:: :py:func:`queryChanged`
%End
void removeQuery( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
%Docstring
Removes the stored query with matching ``name``.
:param name: name of query to remove
:param backend: storage backend for query
.. seealso:: :py:func:`queryRemoved`
%End
QStringList allQueryNames( Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
%Docstring
Returns a list of the names of all stored queries for the specified ``backend``.
%End
QString query( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
%Docstring
Returns the query definition with matching ``name``, from the specified ``backend``.
%End
class QueryDetails
{
%Docstring(signature="appended")
Contains details about a stored query.
.. versionadded:: 3.44
%End
%TypeHeaderCode
#include "qgsstoredquerymanager.h"
%End
public:
QString name;
QString definition;
Qgis::QueryStorageBackend backend;
};
QList< QgsStoredQueryManager::QueryDetails > allQueries() const;
%Docstring
Returns details of all queries stored in the manager.
Queries will be sorted by name.
%End
signals:
void queryAdded( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when a query is added to the manager.
%End
void queryChanged( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when an existing query is changed in the manager.
%End
void queryRemoved( const QString &name, Qgis::QueryStorageBackend backend );
%Docstring
Emitted when a query is removed from the manager.
%End
};
/************************************************************************
* This file has been generated automatically from *
* *
* src/gui/qgsstoredquerymanager.h *
* *
* Do not edit manually ! Edit header and run scripts/sipify.py again *
************************************************************************/

View File

@ -208,6 +208,7 @@
%Include auto_generated/qgssourceselectprovider.sip
%Include auto_generated/qgssourceselectproviderregistry.sip
%Include auto_generated/qgsstatusbar.sip
%Include auto_generated/qgsstoredquerymanager.sip
%Include auto_generated/qgsstyleitemslistwidget.sip
%Include auto_generated/qgssublayersdialog.sip
%Include auto_generated/qgssubstitutionlistwidget.sip

View File

@ -3342,6 +3342,18 @@ class CORE_EXPORT Qgis
Q_DECLARE_FLAGS( HistoryProviderBackends, HistoryProviderBackend )
Q_FLAG( HistoryProviderBackends )
/**
* Stored query storage backends.
*
* \since QGIS 3.44
*/
enum class QueryStorageBackend : int
{
LocalProfile, //!< Local user profile
CurrentProject, //!< Current QGIS project
};
Q_ENUM( QueryStorageBackend )
/**
* Processing data source types.
*

View File

@ -67,6 +67,7 @@ class CORE_EXPORT QgsSettingsTree
static inline QgsSettingsTreeNode *sTreeAttributeTable = treeRoot()->createChildNode( QStringLiteral( "attribute-table" ) );
static inline QgsSettingsTreeNode *sTreeWindowState = sTreeGui->createChildNode( QStringLiteral( "window-state" ) );
static inline QgsSettingsTreeNode *sTreeAuthentication = treeRoot()->createChildNode( QStringLiteral( "authentication" ) );
static inline QgsSettingsTreeNode *sTreeDatabase = treeRoot()->createChildNode( QStringLiteral( "database" ) );
#endif

View File

@ -772,6 +772,7 @@ set(QGIS_GUI_SRCS
qgssqlcomposerdialog.cpp
qgsstackedwidget.cpp
qgsstatusbar.cpp
qgsstoredquerymanager.cpp
qgssymbolbutton.cpp
qgssymbollayerselectionwidget.cpp
qgstabbarproxystyle.cpp
@ -1040,6 +1041,7 @@ set(QGIS_GUI_HDRS
qgssqlcomposerdialog.h
qgsstackedwidget.h
qgsstatusbar.h
qgsstoredquerymanager.h
qgsstyleitemslistwidget.h
qgssublayersdialog.h
qgssubstitutionlistwidget.h

View File

@ -69,7 +69,7 @@
#include "qgssensorguiregistry.h"
#include "qgshistoryentry.h"
#include "qgsstacsourceselectprovider.h"
#include "qgsstoredquerymanager.h"
#include "qgssettingseditorwidgetregistry.h"
@ -226,6 +226,11 @@ QgsInputControllerManager *QgsGui::inputControllerManager()
return instance()->mInputControllerManager;
}
QgsStoredQueryManager *QgsGui::storedQueryManager()
{
return instance()->mStoredQueryManager;
}
void QgsGui::setWindowManager( QgsWindowManagerInterface *manager )
{
instance()->mWindowManager.reset( manager );
@ -271,6 +276,7 @@ QgsGui::~QgsGui()
delete mInputControllerManager;
delete mSettingsRegistryGui;
delete mSensorGuiRegistry;
delete mStoredQueryManager;
delete mSettingsEditorRegistry;
}
@ -324,6 +330,7 @@ QgsGui::QgsGui()
mSettingsEditorRegistry = new QgsSettingsEditorWidgetRegistry();
mStoredQueryManager = new QgsStoredQueryManager();
mCodeEditorColorSchemeRegistry = new QgsCodeEditorColorSchemeRegistry();
// provider gui registry initialize QgsProviderRegistry too

View File

@ -54,6 +54,7 @@ class QgsHistoryProviderRegistry;
class QgsSensorGuiRegistry;
class QgsSettingsEditorWidgetRegistry;
class QgsInputControllerManager;
class QgsStoredQueryManager;
/**
* \ingroup gui
@ -261,6 +262,12 @@ class GUI_EXPORT QgsGui : public QObject
*/
static QgsInputControllerManager *inputControllerManager() SIP_KEEPREFERENCE;
/**
* Returns the global stored SQL query manager.
* \since QGIS 3.44
*/
static QgsStoredQueryManager *storedQueryManager() SIP_KEEPREFERENCE;
/**
* HIG flags, which indicate the Human Interface Guidelines for the current platform.
* \since QGIS 3.4
@ -373,6 +380,7 @@ class GUI_EXPORT QgsGui : public QObject
QgsSensorGuiRegistry *mSensorGuiRegistry = nullptr;
QgsSettingsEditorWidgetRegistry *mSettingsEditorRegistry = nullptr;
QgsInputControllerManager *mInputControllerManager = nullptr;
QgsStoredQueryManager *mStoredQueryManager = nullptr;
std::unique_ptr<QgsWindowManagerInterface> mWindowManager;
#ifdef SIP_RUN

View File

@ -0,0 +1,203 @@
/***************************************************************************
qgsstoredquerymanager.cpp
------------------------------------
Date : February 2025
Copyright : (C) 2025 Nyall Dawson
Email : nyall dot dawson at gmail dot 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 "qgsstoredquerymanager.h"
#include "qgsproject.h"
#include "qgssettingsentryimpl.h"
#include "moc_qgsstoredquerymanager.cpp"
#include <QCryptographicHash>
///@cond PRIVATE
const QgsSettingsEntryString *QgsStoredQueryManager::settingQueryName = new QgsSettingsEntryString( QStringLiteral( "name" ), sTreeStoredQueries );
const QgsSettingsEntryString *QgsStoredQueryManager::settingQueryDefinition = new QgsSettingsEntryString( QStringLiteral( "query" ), sTreeStoredQueries );
///@endcond PRIVATE
QgsStoredQueryManager::QgsStoredQueryManager( QObject *parent )
: QObject( parent )
{
}
void QgsStoredQueryManager::storeQuery( const QString &name, const QString &query, Qgis::QueryStorageBackend backend )
{
if ( query.isEmpty() || name.isEmpty() )
return;
bool wasAdded = false;
bool wasUpdated = false;
// Yes, this looks odd! It's this way for compatibility with DB Manager stored queries...
const QString hash = getQueryHash( name );
switch ( backend )
{
case Qgis::QueryStorageBackend::LocalProfile:
{
const bool isExisting = sTreeStoredQueries->items().contains( hash );
wasAdded = !isExisting;
wasUpdated = isExisting;
settingQueryName->setValue( name, hash );
settingQueryDefinition->setValue( query, hash );
break;
}
case Qgis::QueryStorageBackend::CurrentProject:
{
const bool isExisting = QgsProject::instance()->subkeyList( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries" ) ).contains( hash );
wasAdded = !isExisting;
wasUpdated = isExisting;
QgsProject::instance()->writeEntry( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/name" ).arg( hash ), name );
QgsProject::instance()->writeEntry( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/query" ).arg( hash ), query );
break;
}
}
if ( wasAdded )
emit queryAdded( name, backend );
else if ( wasUpdated )
emit queryChanged( name, backend );
}
void QgsStoredQueryManager::removeQuery( const QString &name, Qgis::QueryStorageBackend backend )
{
if ( name.isEmpty() )
return;
bool wasDeleted = false;
// Yes, this looks odd! It's this way for compatibility with DB Manager stored queries...
const QString hash = getQueryHash( name );
switch ( backend )
{
case Qgis::QueryStorageBackend::LocalProfile:
{
wasDeleted = sTreeStoredQueries->items().contains( hash );
sTreeStoredQueries->deleteItem( hash );
break;
}
case Qgis::QueryStorageBackend::CurrentProject:
{
wasDeleted = QgsProject::instance()->subkeyList( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries" ) ).contains( hash );
QgsProject::instance()->removeEntry(
"DBManager", QStringLiteral( "savedQueries/%1" ).arg( hash )
);
break;
}
}
if ( wasDeleted )
emit queryRemoved( name, backend );
}
QStringList QgsStoredQueryManager::allQueryNames( Qgis::QueryStorageBackend backend ) const
{
QStringList names;
switch ( backend )
{
case Qgis::QueryStorageBackend::LocalProfile:
{
const QStringList hashes = sTreeStoredQueries->items();
names.reserve( hashes.size() );
for ( const QString &hash : hashes )
{
names.append( settingQueryName->value( hash ) );
}
break;
}
case Qgis::QueryStorageBackend::CurrentProject:
{
const QStringList hashes = QgsProject::instance()->subkeyList( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries" ) );
names.reserve( hashes.size() );
for ( const QString &hash : hashes )
{
names.append( QgsProject::instance()->readEntry(
QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/name" ).arg( hash )
) );
}
break;
}
}
return names;
}
QString QgsStoredQueryManager::query( const QString &name, Qgis::QueryStorageBackend backend ) const
{
// Yes, this looks odd! It's this way for compatibility with DB Manager stored queries...
const QString hash = getQueryHash( name );
switch ( backend )
{
case Qgis::QueryStorageBackend::LocalProfile:
{
return settingQueryDefinition->value( hash );
}
case Qgis::QueryStorageBackend::CurrentProject:
{
return QgsProject::instance()->readEntry( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/query" ).arg( hash ) );
}
}
BUILTIN_UNREACHABLE;
}
QList<QgsStoredQueryManager::QueryDetails> QgsStoredQueryManager::allQueries() const
{
QList<QgsStoredQueryManager::QueryDetails> res;
const QStringList localProfileHashes = sTreeStoredQueries->items();
const QStringList projectHashes = QgsProject::instance()->subkeyList( QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries" ) );
res.reserve( localProfileHashes.size() + projectHashes.size() );
for ( const QString &hash : localProfileHashes )
{
QueryDetails details;
details.name = settingQueryName->value( hash );
details.definition = settingQueryDefinition->value( hash );
details.backend = Qgis::QueryStorageBackend::LocalProfile;
res.append( details );
}
for ( const QString &hash : projectHashes )
{
QueryDetails details;
details.name = QgsProject::instance()->readEntry(
QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/name" ).arg( hash )
);
details.definition = QgsProject::instance()->readEntry(
QStringLiteral( "DBManager" ), QStringLiteral( "savedQueries/%1/query" ).arg( hash )
);
details.backend = Qgis::QueryStorageBackend::CurrentProject;
res.append( details );
}
std::sort( res.begin(), res.end(), [=]( const QueryDetails &a, const QueryDetails &b ) {
if ( a.name == b.name )
return a.backend == Qgis::QueryStorageBackend::CurrentProject;
return QString::localeAwareCompare( a.name, b.name ) < 0;
} );
return res;
}
QString QgsStoredQueryManager::getQueryHash( const QString &name )
{
// for compatibility with DB manager stored queries!
QByteArray nameUtf8 = name.toUtf8();
QByteArray hash = QCryptographicHash::hash( nameUtf8, QCryptographicHash::Md5 ).toHex();
return QStringLiteral( "q%1" ).arg( QString::fromUtf8( hash ) );
}

View File

@ -0,0 +1,142 @@
/***************************************************************************
qgsstoredquerymanager.h
----------------------------------
Date : February 2025
Copyright : (C) 2025 Nyall Dawson
Email : nyall dot dawson at gmail dot 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. *
* *
***************************************************************************/
#ifndef QGSSTOREDQUERYMANAGER_H
#define QGSSTOREDQUERYMANAGER_H
#include "qgis.h"
#include "qgis_gui.h"
#include "qgssettingstree.h"
///@cond NOT_STABLE
/**
* \ingroup gui
* \brief A manager for stored SQL queries.
*
* QgsStoredQueryManager is not usually directly created, instead
* use the instance accessible through QgsGui::storedQueryManager().
*
* \since QGIS 3.44
*/
class GUI_EXPORT QgsStoredQueryManager : public QObject
{
Q_OBJECT
public:
#ifndef SIP_RUN
///@cond PRIVATE
static inline QgsSettingsTreeNamedListNode *sTreeStoredQueries = QgsSettingsTree::sTreeDatabase->createNamedListNode( QStringLiteral( "stored-queries" ) );
static const QgsSettingsEntryString *settingQueryName;
static const QgsSettingsEntryString *settingQueryDefinition;
///@endcond
#endif
/**
* Constructor for QgsStoredQueryManager, with the specified
* \a parent object.
*/
QgsStoredQueryManager( QObject *parent = nullptr );
/**
* Saves a query to the manager.
*
* If a query with the same \a name already exists it will be overwritten with the new definition.
*
* \param name user-set, unique name for the query.
* \param query query definition to store
* \param backend storage backend for query
*
* \see queryAdded()
* \see queryChanged()
*/
void storeQuery( const QString &name, const QString &query, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
/**
* Removes the stored query with matching \a name.
*
* \param name name of query to remove
* \param backend storage backend for query
*
* \see queryRemoved()
*/
void removeQuery( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile );
/**
* Returns a list of the names of all stored queries for the specified \a backend.
*/
QStringList allQueryNames( Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
/**
* Returns the query definition with matching \a name, from the specified \a backend.
*/
QString query( const QString &name, Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile ) const;
/**
* \ingroup gui
* \brief Contains details about a stored query.
*
* \since QGIS 3.44
*/
class QueryDetails
{
public:
/**
* Name of the query.
*/
QString name;
/**
* Query definition.
*/
QString definition;
/**
* Storage backend.
*/
Qgis::QueryStorageBackend backend = Qgis::QueryStorageBackend::LocalProfile;
};
/**
* Returns details of all queries stored in the manager.
*
* Queries will be sorted by name.
*/
QList< QgsStoredQueryManager::QueryDetails > allQueries() const;
signals:
/**
* Emitted when a query is added to the manager.
*/
void queryAdded( const QString &name, Qgis::QueryStorageBackend backend );
/**
* Emitted when an existing query is changed in the manager.
*/
void queryChanged( const QString &name, Qgis::QueryStorageBackend backend );
/**
* Emitted when a query is removed from the manager.
*/
void queryRemoved( const QString &name, Qgis::QueryStorageBackend backend );
private:
static QString getQueryHash( const QString &name );
};
///@endcond
#endif // QGSSTOREDQUERYMANAGER_H

View File

@ -334,6 +334,7 @@ ADD_PYTHON_TEST(PyQgsExternalStorageSimpleCopy test_qgsexternalstorage_simplecop
ADD_PYTHON_TEST(PyQgsSpatialIndex test_qgsspatialindex.py)
ADD_PYTHON_TEST(PyQgsSpatialiteProvider test_provider_spatialite.py)
ADD_PYTHON_TEST(PyQgsSQLStatement test_qgssqlstatement.py)
ADD_PYTHON_TEST(PyQgsStoredQueryManager test_qgsstoredquerymanager.py)
ADD_PYTHON_TEST(PyQgsStringStatisticalSummary test_qgsstringstatisticalsummary.py)
ADD_PYTHON_TEST(PyQgsSymbolLayer test_qgssymbollayer.py)
ADD_PYTHON_TEST(PyQgsSymbolBufferSettingsWidget test_qgssymbolbuffersettingswidget.py)

View File

@ -0,0 +1,312 @@
"""QGIS Unit tests for QgsStoredQueryManager.
.. 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.
"""
import unittest
from qgis.PyQt.QtCore import QCoreApplication, QLocale
from qgis.PyQt.QtTest import QSignalSpy
from qgis.core import (
Qgis,
QgsProject,
QgsSettings,
)
from qgis.gui import QgsGui
from qgis.testing import start_app, QgisTestCase
from utilities import unitTestDataPath
start_app()
TEST_DATA_DIR = unitTestDataPath()
class TestQgsStoredQueryManager(QgisTestCase):
@classmethod
def setUpClass(cls):
"""Run before all tests"""
super().setUpClass()
QCoreApplication.setOrganizationName("QGIS_Test")
QCoreApplication.setOrganizationDomain("QGIS_TestQgsStoredQueryManager.com")
QCoreApplication.setApplicationName("QGIS_TestQgsStoredQueryManager")
QgsSettings().clear()
QLocale.setDefault(QLocale(QLocale.Language.English))
start_app()
def test_project_storage(self):
"""
Test storage of queries in a project
"""
p = QgsProject.instance()
manager = QgsGui.storedQueryManager()
QgsSettings().clear()
QgsProject.instance().clear()
self.assertFalse(manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject))
self.assertFalse(manager.query("test"))
signal_added = QSignalSpy(manager.queryAdded)
signal_changed = QSignalSpy(manager.queryChanged)
signal_removed = QSignalSpy(manager.queryRemoved)
manager.storeQuery(
"first query",
"select * from something",
Qgis.QueryStorageBackend.CurrentProject,
)
self.assertEqual(len(signal_added), 1)
self.assertEqual(signal_added[-1][0], "first query")
self.assertEqual(len(signal_changed), 0)
self.assertEqual(len(signal_removed), 0)
self.assertEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject),
["first query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.CurrentProject),
"select * from something",
)
self.assertFalse(manager.query("test"))
manager.storeQuery(
"ANOTHER query",
"""INSERT INTO Students (name) VALUES ('Robert');
DROP TABLE Students;--');""",
Qgis.QueryStorageBackend.CurrentProject,
)
self.assertEqual(len(signal_added), 2)
self.assertEqual(signal_added[-1][0], "ANOTHER query")
self.assertEqual(len(signal_changed), 0)
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject),
["first query", "ANOTHER query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.CurrentProject),
"select * from something",
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.CurrentProject),
"""INSERT INTO Students (name) VALUES ('Robert');
DROP TABLE Students;--');""",
)
self.assertFalse(manager.query("test"))
# update an existing query
manager.storeQuery(
"ANOTHER query",
"DROP TABLE Students",
Qgis.QueryStorageBackend.CurrentProject,
)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(signal_changed[-1][0], "ANOTHER query")
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject),
["first query", "ANOTHER query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.CurrentProject),
"select * from something",
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.CurrentProject),
"DROP TABLE Students",
)
self.assertFalse(manager.query("test"))
# remove a query which doesn't exist
manager.removeQuery("xxx", Qgis.QueryStorageBackend.CurrentProject)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject),
["first query", "ANOTHER query"],
)
# remove a query
manager.removeQuery("first query", Qgis.QueryStorageBackend.CurrentProject)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(len(signal_removed), 1)
self.assertEqual(signal_removed[-1][0], "first query")
self.assertEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject),
["ANOTHER query"],
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.CurrentProject),
"DROP TABLE Students",
)
p.clear()
self.assertFalse(manager.allQueryNames(Qgis.QueryStorageBackend.CurrentProject))
def test_local_profile_storage(self):
"""
Test storage of queries in the local profile
"""
QgsSettings().clear()
QgsProject.instance().clear()
manager = QgsGui.storedQueryManager()
self.assertFalse(manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile))
self.assertFalse(manager.query("test"))
signal_added = QSignalSpy(manager.queryAdded)
signal_changed = QSignalSpy(manager.queryChanged)
signal_removed = QSignalSpy(manager.queryRemoved)
manager.storeQuery(
"first query",
"select * from something",
Qgis.QueryStorageBackend.LocalProfile,
)
self.assertEqual(len(signal_added), 1)
self.assertEqual(signal_added[-1][0], "first query")
self.assertEqual(len(signal_changed), 0)
self.assertEqual(len(signal_removed), 0)
self.assertEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile),
["first query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.LocalProfile),
"select * from something",
)
self.assertFalse(manager.query("test"))
manager.storeQuery(
"ANOTHER query",
"""INSERT INTO Students (name) VALUES ('Robert');
DROP TABLE Students;--');""",
Qgis.QueryStorageBackend.LocalProfile,
)
self.assertEqual(len(signal_added), 2)
self.assertEqual(signal_added[-1][0], "ANOTHER query")
self.assertEqual(len(signal_changed), 0)
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile),
["first query", "ANOTHER query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.LocalProfile),
"select * from something",
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.LocalProfile),
"""INSERT INTO Students (name) VALUES ('Robert');
DROP TABLE Students;--');""",
)
self.assertFalse(manager.query("test"))
# update an existing query
manager.storeQuery(
"ANOTHER query",
"DROP TABLE Students",
Qgis.QueryStorageBackend.LocalProfile,
)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(signal_changed[-1][0], "ANOTHER query")
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile),
["first query", "ANOTHER query"],
)
self.assertEqual(
manager.query("first query", Qgis.QueryStorageBackend.LocalProfile),
"select * from something",
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.LocalProfile),
"DROP TABLE Students",
)
self.assertFalse(manager.query("test"))
# remove a query which doesn't exist
manager.removeQuery("xxx", Qgis.QueryStorageBackend.LocalProfile)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(len(signal_removed), 0)
self.assertCountEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile),
["first query", "ANOTHER query"],
)
# remove a query
manager.removeQuery("first query", Qgis.QueryStorageBackend.LocalProfile)
self.assertEqual(len(signal_added), 2)
self.assertEqual(len(signal_changed), 1)
self.assertEqual(len(signal_removed), 1)
self.assertEqual(signal_removed[-1][0], "first query")
self.assertEqual(
manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile),
["ANOTHER query"],
)
self.assertEqual(
manager.query("ANOTHER query", Qgis.QueryStorageBackend.LocalProfile),
"DROP TABLE Students",
)
QgsSettings().clear()
self.assertFalse(manager.allQueryNames(Qgis.QueryStorageBackend.LocalProfile))
def test_all_queries(self):
"""
Test retrieving all queries.
"""
QgsSettings().clear()
QgsProject.instance().clear()
manager = QgsGui.storedQueryManager()
manager.storeQuery(
"first project query",
"select * from something",
Qgis.QueryStorageBackend.CurrentProject,
)
manager.storeQuery(
"SECOND project query",
"select * from something_else",
Qgis.QueryStorageBackend.CurrentProject,
)
manager.storeQuery(
"first PROFILE query",
"select * from something3",
Qgis.QueryStorageBackend.LocalProfile,
)
manager.storeQuery(
"2 PROFILE query",
"select * from something4",
Qgis.QueryStorageBackend.LocalProfile,
)
res = manager.allQueries()
self.assertEqual(len(res), 4)
self.assertEqual(res[0].name, "2 PROFILE query")
self.assertEqual(res[0].definition, "select * from something4")
self.assertEqual(res[0].backend, Qgis.QueryStorageBackend.LocalProfile)
self.assertEqual(res[1].name, "first PROFILE query")
self.assertEqual(res[1].definition, "select * from something3")
self.assertEqual(res[1].backend, Qgis.QueryStorageBackend.LocalProfile)
self.assertEqual(res[2].name, "first project query")
self.assertEqual(res[2].definition, "select * from something")
self.assertEqual(res[2].backend, Qgis.QueryStorageBackend.CurrentProject)
self.assertEqual(res[3].name, "SECOND project query")
self.assertEqual(res[3].definition, "select * from something_else")
self.assertEqual(res[3].backend, Qgis.QueryStorageBackend.CurrentProject)
if __name__ == "__main__":
unittest.main()