Skip to content

Commit

Permalink
Merge pull request #38320 from qgis-bot/backport-38257-to-release-3_14
Browse files Browse the repository at this point in the history
[Backport release-3_14] Bugfix gh26189 virtual layers subset string
  • Loading branch information
elpaso committed Aug 17, 2020
2 parents 34ec910 + 1026624 commit af1f013
Show file tree
Hide file tree
Showing 5 changed files with 158 additions and 5 deletions.
15 changes: 15 additions & 0 deletions python/core/auto_generated/qgsvirtuallayerdefinition.sip.in
Expand Up @@ -94,6 +94,7 @@ uid=column_name is the name of a column with unique inte
nogeometry is a flag to force the layer to be a non-geometry layer
query=sql represents the SQL query. Must be URL-encoded
field=column_name:[int|real|text] represents a field with its name and its type
subsetstring=subset_string represents the subsetstring
%End

QUrl toUrl() const;
Expand Down Expand Up @@ -226,6 +227,20 @@ Convenience method to test whether the definition has referenced (live) layers
bool hasDefinedGeometry() const;
%Docstring
Convenient method to test if the geometry is defined (not NoGeometry and not Unknown)
%End

QString subsetString() const;
%Docstring
Returns the subset string.

.. versionadded:: 3.16
%End

void setSubsetString( const QString &subsetString );
%Docstring
Sets the ``subsetString``

.. versionadded:: 3.16
%End

};
Expand Down
19 changes: 19 additions & 0 deletions src/core/qgsvirtuallayerdefinition.cpp
Expand Up @@ -162,6 +162,10 @@ QgsVirtualLayerDefinition QgsVirtualLayerDefinition::fromUrl( const QUrl &url )
{
def.setLazy( true );
}
else if ( key == QLatin1String( "subsetstring" ) )
{
def.setSubsetString( QUrl::fromPercentEncoding( value.toUtf8() ) );
}
}
def.setFields( fields );

Expand Down Expand Up @@ -302,6 +306,11 @@ QUrl QgsVirtualLayerDefinition::toUrl() const
urlQuery.addQueryItem( QStringLiteral( "lazy" ), QString() );
}

if ( ! subsetString().isEmpty() )
{
urlQuery.addQueryItem( QStringLiteral( "subsetstring" ), QUrl::toPercentEncoding( subsetString() ) );
}

url.setQuery( urlQuery );

return url;
Expand Down Expand Up @@ -347,3 +356,13 @@ bool QgsVirtualLayerDefinition::hasReferencedLayers() const
}
return false;
}

QString QgsVirtualLayerDefinition::subsetString() const
{
return mSubsetString;
}

void QgsVirtualLayerDefinition::setSubsetString( const QString &subsetString )
{
mSubsetString = subsetString;
}
14 changes: 14 additions & 0 deletions src/core/qgsvirtuallayerdefinition.h
Expand Up @@ -95,6 +95,7 @@ class CORE_EXPORT QgsVirtualLayerDefinition
* nogeometry is a flag to force the layer to be a non-geometry layer
* query=sql represents the SQL query. Must be URL-encoded
* field=column_name:[int|real|text] represents a field with its name and its type
* subsetstring=subset_string represents the subsetstring
*/
static QgsVirtualLayerDefinition fromUrl( const QUrl &url );

Expand Down Expand Up @@ -185,6 +186,18 @@ class CORE_EXPORT QgsVirtualLayerDefinition
return geometryWkbType() != QgsWkbTypes::NoGeometry && geometryWkbType() != QgsWkbTypes::Unknown;
}

/**
* Returns the subset string.
* \since QGIS 3.16
*/
QString subsetString() const;

/**
* Sets the \a subsetString
* \since QGIS 3.16
*/
void setSubsetString( const QString &subsetString );

private:
SourceLayers mSourceLayers;
QString mQuery;
Expand All @@ -195,6 +208,7 @@ class CORE_EXPORT QgsVirtualLayerDefinition
bool mLazy = false;
QgsWkbTypes::Type mGeometryWkbType = QgsWkbTypes::Unknown;
long mGeometrySrid = 0;
QString mSubsetString;
};

// clazy:excludeall=qstring-allocations
Expand Down
8 changes: 8 additions & 0 deletions src/providers/virtual/qgsvirtuallayerprovider.cpp
Expand Up @@ -76,6 +76,8 @@ QgsVirtualLayerProvider::QgsVirtualLayerProvider( QString const &uri, const QgsD
{
mDefinition = QgsVirtualLayerDefinition::fromUrl( url );

mSubset = mDefinition.subsetString();

if ( !mDefinition.isLazy() )
{
reloadData();
Expand Down Expand Up @@ -205,6 +207,8 @@ bool QgsVirtualLayerProvider::openIt()
mTableName = VIRTUAL_LAYER_QUERY_VIEW;
}

mSubset = mDefinition.subsetString();

return true;
}

Expand Down Expand Up @@ -523,6 +527,10 @@ bool QgsVirtualLayerProvider::setSubsetString( const QString &subset, bool updat
if ( updateFeatureCount )
updateStatistics();

mDefinition.setSubsetString( subset );

setDataSourceUri( mDefinition.toString() );

emit dataChanged();

return true;
Expand Down
107 changes: 102 additions & 5 deletions tests/src/python/test_provider_virtual.py
Expand Up @@ -23,14 +23,16 @@
QgsVirtualLayerDefinitionUtils,
QgsWkbTypes,
QgsProject,
QgsVectorLayerJoinInfo
QgsVectorLayerJoinInfo,
QgsVectorFileWriter,
QgsVirtualLayerDefinitionUtils
)

from qgis.testing import start_app, unittest
from utilities import unitTestDataPath

from providertestbase import ProviderTestCase
from qgis.PyQt.QtCore import QUrl, QVariant
from qgis.PyQt.QtCore import QUrl, QVariant, QTemporaryDir

from qgis.utils import spatialite_connect

Expand Down Expand Up @@ -833,15 +835,13 @@ def test_relative_paths(self):
QgsProject.instance().setFileName(temp)
QgsProject.instance().write()

QgsProject.instance().removeAllMapLayers()
QgsProject.instance().clear()
self.assertEqual(len(QgsProject.instance().mapLayers()), 0)

# Check that virtual layer source is stored with relative path
percent_path_relative = toPercent("./france_parts.shp")
with open(temp, 'r') as f:
content = ''.join(f.readlines())
print(content)
self.assertTrue('<datasource>?layer=ogr:{}'.format(percent_path_relative) in content)

# Check that project is correctly re-read with all layers
Expand All @@ -854,7 +854,6 @@ def test_relative_paths(self):
QgsProject.instance().writeEntryBool('Paths', '/Absolute', True)
QgsProject.instance().write()

QgsProject.instance().removeAllMapLayers()
QgsProject.instance().clear()
self.assertEqual(len(QgsProject.instance().mapLayers()), 0)

Expand Down Expand Up @@ -1164,6 +1163,104 @@ def test_filter_rect_precise(self):

QgsProject.instance().removeMapLayer(pl)

def test_subset_string(self):
"""Test that subset strings are stored and restored correctly from the project
See: GH #26189
"""

project = QgsProject.instance()
project.clear()
data_layer = QgsVectorLayer('Point?crs=epsg:4326&field=fid:integer&field=value:integer&field=join_pk:integer', 'data', 'memory')
join_layer = QgsVectorLayer('NoGeometry?field=fid:integer&field=value:string', 'join', 'memory')
tempdir = QTemporaryDir()
gpkg_path = os.path.join(tempdir.path(), 'test_subset.gpkg')
project_path = os.path.join(tempdir.path(), 'test_subset.qgs')
self.assertTrue(data_layer.isValid())
self.assertTrue(join_layer.isValid())
self.assertFalse(join_layer.isSpatial())

f = QgsFeature(data_layer.fields())
f.setAttributes([1, 20, 2])
f.setGeometry(QgsGeometry.fromWkt('point(9 45'))
self.assertTrue(data_layer.dataProvider().addFeature(f))

f = QgsFeature(data_layer.fields())
f.setAttributes([2, 10, 1])
f.setGeometry(QgsGeometry.fromWkt('point(9 45'))
self.assertTrue(data_layer.dataProvider().addFeature(f))

options = QgsVectorFileWriter.SaveVectorOptions()
options.driverName = 'GPKG'
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteFile
options.layerName = 'data'

_, _ = QgsVectorFileWriter.writeAsVectorFormatV2(
data_layer,
gpkg_path,
data_layer.transformContext(),
options
)

f = QgsFeature(join_layer.fields())
f.setAttributes([1, "ten"])
self.assertTrue(join_layer.dataProvider().addFeature(f))
f.setAttributes([2, "twenty"])
self.assertTrue(join_layer.dataProvider().addFeature(f))

options.layerName = 'join'
options.actionOnExistingFile = QgsVectorFileWriter.CreateOrOverwriteLayer

_, _ = QgsVectorFileWriter.writeAsVectorFormatV2(
join_layer,
gpkg_path,
join_layer.transformContext(),
options
)

gpkg_join_layer = QgsVectorLayer(gpkg_path + '|layername=join', 'join', 'ogr')
gpkg_data_layer = QgsVectorLayer(gpkg_path + '|layername=data', 'data', 'ogr')

self.assertTrue(gpkg_join_layer.isValid())
self.assertTrue(gpkg_data_layer.isValid())
self.assertEqual(gpkg_data_layer.featureCount(), 2)
self.assertEqual(gpkg_join_layer.featureCount(), 2)

self.assertTrue(project.addMapLayers([gpkg_data_layer, gpkg_join_layer]))

joinInfo = QgsVectorLayerJoinInfo()
joinInfo.setTargetFieldName("join_pk")
joinInfo.setJoinLayer(gpkg_join_layer)
joinInfo.setJoinFieldName("fid")
gpkg_data_layer.addJoin(joinInfo)
self.assertEqual(len(gpkg_data_layer.fields()), 4)

self.assertTrue(project.write(project_path))

# Reload project
self.assertTrue(project.read(project_path))
gpkg_data_layer = project.mapLayersByName('data')[0]
gpkg_join_layer = project.mapLayersByName('join')[0]

self.assertEqual(gpkg_data_layer.vectorJoins()[0], joinInfo)

# Now set a subset filter -> virtual layer
virtual_def = QgsVirtualLayerDefinitionUtils.fromJoinedLayer(gpkg_data_layer)
virtual = QgsVectorLayer(virtual_def.toString(), "virtual_data", "virtual")
self.assertTrue(virtual.isValid())
project.addMapLayers([virtual])

self.assertEqual(virtual.featureCount(), 2)
self.assertTrue(virtual.setSubsetString('"join_value" = \'twenty\''))
self.assertEqual(virtual.featureCount(), 1)
self.assertEqual([f.attributes() for f in virtual.getFeatures()], [[1, 20, 2, 'twenty']])

# Store and reload the project
self.assertTrue(project.write(project_path))
self.assertTrue(project.read(project_path))
gpkg_virtual_layer = project.mapLayersByName('virtual_data')[0]
self.assertEqual(gpkg_virtual_layer.featureCount(), 1)
self.assertEqual(gpkg_virtual_layer.subsetString(), '"join_value" = \'twenty\'')


if __name__ == '__main__':
unittest.main()

0 comments on commit af1f013

Please sign in to comment.