Skip to content
Permalink
Browse files
Add isQuery() to vector layer and data provider
The method returns TRUE if the layer is a query/sql layer.

This allows to selectively show the "Update SQL Layer..." menu
entry in the legend.

Partial fix for #45796
  • Loading branch information
elpaso authored and nyalldawson committed Nov 4, 2021
1 parent cf467c4 commit a04112d740d4b76a7d247140bfcb8bafb8e0ca9a
@@ -128,6 +128,19 @@ Returns ``True`` if the layer does not contain any feature.
.. versionadded:: 3.4
%End

virtual bool isQuery() const;
%Docstring
Returns ``True`` if the layer is a query (SQL) layer.

.. note::

the default implementation return ``False``, individual providers
must override if they support query layers.

.. versionadded:: 3.24
%End


virtual QgsFeatureSource::FeatureAvailability hasFeatures() const;

%Docstring
@@ -451,6 +451,14 @@ This is obtained from the data provider and does not follow any standard.
Capabilities for this layer, comma separated and translated.
%End


virtual bool isQuery() const;
%Docstring
Returns ``True`` if the layer is a query (SQL) layer.

.. versionadded:: 3.24
%End

QString dataComment() const;
%Docstring
Returns a description for this layer as defined in the data provider.
@@ -222,7 +222,7 @@ QMenu *QgsAppLayerTreeViewMenuProvider::createContextMenu()
}

// No raster support in createSqlVectorLayer (yet)
if ( vlayer )
if ( vlayer && vlayer->isQuery() )
{
const std::unique_ptr< QgsAbstractDatabaseProviderConnection> conn { QgsMapLayerUtils::databaseConnection( layer ) };
if ( conn )
@@ -3174,6 +3174,11 @@ QgsFeatureSource::SpatialIndexPresence QgsOgrProvider::hasSpatialIndex() const
return QgsFeatureSource::SpatialIndexUnknown;
}

bool QgsOgrProvider::isQuery() const
{
return mSubsetString.trimmed().startsWith( QStringLiteral( "SELECT" ), Qt::CaseSensitivity::CaseInsensitive );
}

QVariant QgsOgrProvider::minimumValue( int index ) const
{
if ( !mValid || index < 0 || index >= mAttributeFields.count() )
@@ -124,6 +124,7 @@ class QgsOgrProvider final: public QgsVectorDataProvider
QStringList uniqueStringsMatching( int index, const QString &substring, int limit = -1,
QgsFeedback *feedback = nullptr ) const override;
QgsFeatureSource::SpatialIndexPresence hasSpatialIndex() const override;
bool isQuery() const override;

QString name() const override;
static QString providerKey();
@@ -61,6 +61,11 @@ bool QgsVectorDataProvider::empty() const
return true;
}

bool QgsVectorDataProvider::isQuery() const
{
return false;
}

QgsFeatureSource::FeatureAvailability QgsVectorDataProvider::hasFeatures() const
{
if ( empty() )
@@ -164,6 +164,17 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider, public QgsFeat
*/
virtual bool empty() const;

/**
* Returns TRUE if the layer is a query (SQL) layer.
*
* \note the default implementation return FALSE, individual providers
* must override if they support query layers.
*
* \since QGIS 3.24
*/
virtual bool isQuery() const;


/**
* Will always return FeatureAvailability::FeaturesAvailable or
* FeatureAvailability::NoFeaturesAvailable.
@@ -372,6 +372,11 @@ QString QgsVectorLayer::capabilitiesString() const
return QString();
}

bool QgsVectorLayer::isQuery() const
{
return mDataProvider && mDataProvider->isQuery();
}

QString QgsVectorLayer::dataComment() const
{
if ( mDataProvider )
@@ -570,6 +570,14 @@ class CORE_EXPORT QgsVectorLayer : public QgsMapLayer, public QgsExpressionConte
*/
QString capabilitiesString() const;


/**
* Returns TRUE if the layer is a query (SQL) layer.
*
* \since QGIS 3.24
*/
virtual bool isQuery() const;

/**
* Returns a description for this layer as defined in the data provider.
*/
@@ -46,15 +46,15 @@ using namespace std;

namespace
{
bool isQuery( const QString &source )
bool sourceIsQuery( const QString &source )
{
QString trimmed = source.trimmed();
return trimmed.startsWith( '(' ) && trimmed.endsWith( ')' );
}

QString buildQuery( const QString &source, const QString &columns, const QString &where, const QString &orderBy, int limit )
{
if ( isQuery( source ) && columns == QLatin1String( "*" ) && where.isEmpty() && limit <= 0 )
if ( sourceIsQuery( source ) && columns == QLatin1String( "*" ) && where.isEmpty() && limit <= 0 )
return source;

QString sql = QStringLiteral( "SELECT %1 FROM %2" ).arg( columns, source );
@@ -385,7 +385,7 @@ QgsHanaProvider::QgsHanaProvider(
return;
}

if ( isQuery( mTableName ) )
if ( sourceIsQuery( mTableName ) )
{
mIsQuery = true;
mQuerySource = mTableName;
@@ -1433,7 +1433,7 @@ void QgsHanaProvider::readGeometryType( QgsHanaConnection &conn )
if ( mIsQuery )
{
QString query = buildQuery( QStringLiteral( "*" ) );
if ( !isQuery( query ) )
if ( !sourceIsQuery( query ) )
query = "(" + query + ")";
mDetectedGeometryType = conn.getColumnGeometryType( query, mGeometryColumn );
}
@@ -1555,6 +1555,11 @@ void QgsHanaProvider::updateFeatureIdMap( QgsFeatureId fid, const QgsAttributeMa
mPrimaryKeyCntx->insertFid( fid, values );
}

bool QgsHanaProvider::isQuery() const
{
return mIsQuery;
}

QgsCoordinateReferenceSystem QgsHanaProvider::crs() const
{
static QMutex sMutex;
@@ -102,6 +102,8 @@ class QgsHanaProvider final : public QgsVectorDataProvider
const QMap<QString, QVariant> *options = nullptr
);

bool isQuery() const override;

private:
QgsHanaConnectionRef createConnection() const;
QString buildQuery( const QString &columns, const QString &where, const QString &orderBy, int limit ) const;
@@ -2814,6 +2814,11 @@ bool QgsOracleProvider::convertField( QgsField &field )
return true;
}

bool QgsOracleProvider::isQuery() const
{
return mIsQuery;
}

Qgis::VectorExportResult QgsOracleProvider::createEmptyLayer(
const QString &uri,
const QgsFields &fields,
@@ -189,6 +189,8 @@ class QgsOracleProvider final: public QgsVectorDataProvider
*/
QString getWorkspace() const;

bool isQuery() const override;

private:
QString whereClause( QgsFeatureId featureId, QVariantList &args ) const;
QString pkParamWhereClause() const;
@@ -315,6 +315,11 @@ void QgsPostgresProvider::reloadProviderData()
mLayerExtent.setMinimal();
}

bool QgsPostgresProvider::isQuery() const
{
return mIsQuery;
}


QgsPostgresConn *QgsPostgresProvider::connectionRW()
{
@@ -243,6 +243,8 @@ class QgsPostgresProvider final: public QgsVectorDataProvider
*/
void setListening( bool isListening ) override;

bool isQuery() const override;

private:

/**
@@ -5962,6 +5962,11 @@ void QgsSpatiaLiteProvider::setTransaction( QgsTransaction *transaction )
mTransaction = static_cast<QgsSpatiaLiteTransaction *>( transaction );
}

bool QgsSpatiaLiteProvider::isQuery() const
{
return mIsQuery;
}

QgsTransaction *QgsSpatiaLiteProvider::transaction( ) const
{
return static_cast<QgsTransaction *>( mTransaction );
@@ -414,6 +414,7 @@ class QgsSpatiaLiteProvider final: public QgsVectorDataProvider
// QgsVectorDataProvider interface
public:
virtual QString defaultValueClause( int fieldIndex ) const override;
bool isQuery() const override;
};

class QgsSpatiaLiteProviderMetadata final: public QgsProviderMetadata
@@ -43,7 +43,7 @@
QgsVectorDataProvider,
QgsLayerMetadata,
NULL)
from qgis.PyQt.QtCore import QCoreApplication, QVariant, QDate, QTime, QDateTime, Qt
from qgis.PyQt.QtCore import QCoreApplication, QVariant, QDate, QTime, QDateTime, Qt, QTemporaryDir
from qgis.PyQt.QtXml import QDomDocument
from qgis.testing import start_app, unittest
from qgis.utils import spatialite_connect
@@ -2337,6 +2337,34 @@ def testRejectedGeometryUpdate(self):
g = [f.geometry() for f in vl.getFeatures()][0]
self.assertEqual(g.asWkt(), 'Polygon ((0 0, 0 1, 1 1, 1 0, 0 0))')

def testIsQuery(self):
"""Test that isQuery returns what it should in case of simple filters"""

tmp_dir = QTemporaryDir()
tmpfile = os.path.join(tmp_dir.path(), 'testQueryLayers.gpkg')
ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile)
lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint)
lyr.CreateField(ogr.FieldDefn('text_field', ogr.OFTString))
f = ogr.Feature(lyr.GetLayerDefn())
f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(0 0)'))
f['text_field'] = 'one'
lyr.CreateFeature(f)
f = ogr.Feature(lyr.GetLayerDefn())
f['text_field'] = 'two'
f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(1 1)'))
lyr.CreateFeature(f)
del (lyr)

vl1 = QgsVectorLayer(f'{tmpfile}|subset=SELECT * FROM test WHERE "text_field"=\'one\''.format(tmpfile), 'test', 'ogr')
self.assertTrue(vl1.isValid())
self.assertTrue(vl1.isQuery())
self.assertEqual(vl1.featureCount(), 1)

vl2 = QgsVectorLayer(f'{tmpfile}|subset="text_field"=\'one\''.format(tmpfile), 'test', 'ogr')
self.assertTrue(vl2.isValid())
self.assertFalse(vl2.isQuery())
self.assertEqual(vl2.featureCount(), 1)


if __name__ == '__main__':
unittest.main()
@@ -558,6 +558,7 @@ def testCreateSqlVectorLayer(self):
options.primaryKeyColumns = table_info.primaryKeyColumns()

vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isQuery())
self.assertTrue(vl.isValid())
self.assertTrue(vl.isSpatial())
self.assertEqual(vl.name(), options.layerName)
@@ -571,6 +572,7 @@ def testCreateSqlVectorLayer(self):
if sql_layer_capabilities & Qgis.SqlLayerDefinitionCapability.PrimaryKeys:
options.primaryKeyColumns = []
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isQuery())
self.assertTrue(vl.isValid())
self.assertTrue(vl.isSpatial())

@@ -580,6 +582,7 @@ def testCreateSqlVectorLayer(self):
options.geometryColumn = ''
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
# This may fail for OGR where the provider is smart enough to guess the geometry column
if self.providerKey != 'ogr':
self.assertFalse(vl.isSpatial())
@@ -591,4 +594,5 @@ def testCreateSqlVectorLayer(self):
options.filter = f'"{options.primaryKeyColumns[0]}" > 0'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertTrue(vl.isSpatial())
@@ -477,20 +477,23 @@ def test_create_vector_layer(self):
options.geometryColumn = 'geom'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 0)

options.sql = 'SELECT id, geom FROM qgis_test.query_layer1 WHERE id > 200 LIMIT 2'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 2)

options.sql = 'SELECT id, geom FROM qgis_test.query_layer1 WHERE id > 210 LIMIT 2'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)
@@ -499,6 +502,7 @@ def test_create_vector_layer(self):
options.filter = 'id > 210'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)
@@ -507,17 +511,20 @@ def test_create_vector_layer(self):
options.primaryKeyColumns = ['DOES_NOT_EXIST']
vl = conn.createSqlVectorLayer(options)
self.assertFalse(vl.isValid())
self.assertFalse(vl.isQuery())

options.primaryKeyColumns = ['id']
options.geometryColumn = 'DOES_NOT_EXIST'
vl = conn.createSqlVectorLayer(options)
self.assertFalse(vl.isValid())
self.assertFalse(vl.isQuery())

options.sql = 'SELECT id, geom FROM qgis_test.query_layer1 WHERE id > 210 LIMIT 2'
options.primaryKeyColumns = []
options.geometryColumn = ''
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)

@@ -527,6 +534,7 @@ def test_create_vector_layer(self):
options.geometryColumn = ''
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertNotEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)
@@ -552,6 +560,7 @@ def test_create_vector_layer(self):
options.geometryColumn = 'geom'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isValid())
self.assertTrue(vl.isQuery())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
self.assertEqual(len(features), 1)
@@ -560,6 +569,7 @@ def test_create_vector_layer(self):
options.primaryKeyColumns = []
options.geometryColumn = 'geom'
vl = conn.createSqlVectorLayer(options)
self.assertTrue(vl.isQuery())
self.assertTrue(vl.isValid())
self.assertEqual(vl.geometryType(), QgsWkbTypes.PointGeometry)
features = [f for f in vl.getFeatures()]
Loading

0 comments on commit a04112d

Please sign in to comment.