Skip to content

Commit a733ace

Browse files
authoredOct 20, 2017
Merge pull request #5409 from rouault/fix_gpkg_many_layers
[OGR provider] Allow opening (GeoPackage) datasets with many layers
2 parents e59f1d5 + c9b0a2b commit a733ace

File tree

6 files changed

+1763
-607
lines changed

6 files changed

+1763
-607
lines changed
 

‎src/providers/ogr/qgsogrconnpool.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ inline void qgsConnectionPool_ConnectionCreate( const QString &connInfo, QgsOgrC
3737
{
3838
c = new QgsOgrConn;
3939
QString filePath = connInfo.left( connInfo.indexOf( QLatin1String( "|" ) ) );
40-
c->ds = GDALOpenEx( filePath.toUtf8().constData(), GDAL_OF_VECTOR, nullptr, nullptr, nullptr );
40+
c->ds = QgsOgrProviderUtils::GDALOpenWrapper( filePath.toUtf8().constData(), false, nullptr, nullptr );
4141
c->path = connInfo;
4242
c->valid = true;
4343
}

‎src/providers/ogr/qgsogrdataitems.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ QgsOgrLayerItem::QgsOgrLayerItem( QgsDataItem *parent,
5454

5555
OGRRegisterAll();
5656
GDALDriverH hDriver;
57-
GDALDatasetH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( mPath.toUtf8().constData(), true, false, &hDriver );
57+
GDALDatasetH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( mPath.toUtf8().constData(), true, nullptr, &hDriver );
5858

5959
if ( hDataSource )
6060
{
@@ -408,7 +408,7 @@ QVector<QgsDataItem *> QgsOgrDataCollectionItem::createChildren()
408408
QVector<QgsDataItem *> children;
409409

410410
GDALDriverH hDriver;
411-
GDALDatasetH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( mPath.toUtf8().constData(), false, false, &hDriver );
411+
GDALDatasetH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( mPath.toUtf8().constData(), false, nullptr, &hDriver );
412412
if ( !hDataSource )
413413
return children;
414414
int numLayers = GDALDatasetGetLayerCount( hDataSource );
@@ -639,7 +639,7 @@ QGISEXTERN QgsDataItem *dataItem( QString path, QgsDataItem *parentItem )
639639
// do not print errors, but write to debug
640640
CPLPushErrorHandler( CPLQuietErrorHandler );
641641
CPLErrorReset();
642-
OGRDataSourceH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( path.toUtf8().constData(), false, false, &hDriver );
642+
OGRDataSourceH hDataSource = QgsOgrProviderUtils::GDALOpenWrapper( path.toUtf8().constData(), false, nullptr, &hDriver );
643643
CPLPopErrorHandler();
644644

645645
if ( ! hDataSource )

‎src/providers/ogr/qgsogrfeatureiterator.cpp

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,8 @@ QgsOgrFeatureIterator::QgsOgrFeatureIterator( QgsOgrFeatureSource *source, bool
4747
, mFilterFids( mRequest.filterFids() )
4848
, mFilterFidsIt( mFilterFids.constBegin() )
4949
{
50-
mConn = QgsOgrConnPool::instance()->acquireConnection( mSource->mDataSource );
50+
//QgsDebugMsg( "Feature iterator of " + mSource->mLayerName + ": acquiring connection");
51+
mConn = QgsOgrConnPool::instance()->acquireConnection( QgsOgrProviderUtils::connectionPoolId( mSource->mDataSource ) );
5152
if ( !mConn->ds )
5253
{
5354
return;
@@ -316,7 +317,10 @@ bool QgsOgrFeatureIterator::close()
316317
}
317318

318319
if ( mConn )
320+
{
321+
//QgsDebugMsg( "Feature iterator of " + mSource->mLayerName + ": releasing connection");
319322
QgsOgrConnPool::instance()->releaseConnection( mConn );
323+
}
320324

321325
mConn = nullptr;
322326
ogrLayer = nullptr;
@@ -437,12 +441,12 @@ QgsOgrFeatureSource::QgsOgrFeatureSource( const QgsOgrProvider *p )
437441
{
438442
for ( int i = ( p->mFirstFieldIsFid ) ? 1 : 0; i < mFields.size(); i++ )
439443
mFieldsWithoutFid.append( mFields.at( i ) );
440-
QgsOgrConnPool::instance()->ref( mDataSource );
444+
QgsOgrConnPool::instance()->ref( QgsOgrProviderUtils::connectionPoolId( mDataSource ) );
441445
}
442446

443447
QgsOgrFeatureSource::~QgsOgrFeatureSource()
444448
{
445-
QgsOgrConnPool::instance()->unref( mDataSource );
449+
QgsOgrConnPool::instance()->unref( QgsOgrProviderUtils::connectionPoolId( mDataSource ) );
446450
}
447451

448452
QgsFeatureIterator QgsOgrFeatureSource::getFeatures( const QgsFeatureRequest &request )

‎src/providers/ogr/qgsogrprovider.cpp

Lines changed: 1396 additions & 588 deletions
Large diffs are not rendered by default.

‎src/providers/ogr/qgsogrprovider.h

Lines changed: 271 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,11 @@ class QgsOgrFeatureIterator;
3232

3333
#include <gdal.h>
3434

35+
class QgsOgrLayer;
36+
3537
/**
3638
\class QgsOgrProvider
37-
\brief Data provider for ESRI shapefiles
39+
\brief Data provider for OGR datasources
3840
*/
3941
class QgsOgrProvider : public QgsVectorDataProvider
4042
{
@@ -141,7 +143,7 @@ class QgsOgrProvider : public QgsVectorDataProvider
141143
void recalculateFeatureCount();
142144

143145
//! Tell OGR, which fields to fetch in nextFeature/featureAtId (ie. which not to ignore)
144-
void setRelevantFields( OGRLayerH ogrLayer, bool fetchGeometry, const QgsAttributeList &fetchAttributes );
146+
void setRelevantFields( bool fetchGeometry, const QgsAttributeList &fetchAttributes );
145147

146148
//! Convert a QgsField to work with OGR
147149
static bool convertField( QgsField &field, const QTextCodec &encoding );
@@ -176,22 +178,27 @@ class QgsOgrProvider : public QgsVectorDataProvider
176178
//! Commits a transaction
177179
bool commitTransaction();
178180

181+
void addSubLayerDetailsToSubLayerList( int i, QgsOgrLayer *layer ) const;
182+
179183
QgsFields mAttributeFields;
180184

181185
//! Map of field index to default value
182186
QMap<int, QString> mDefaultValues;
183187

184188
bool mFirstFieldIsFid = false;
185-
GDALDatasetH mGDALDataset = nullptr;
186189
mutable OGREnvelope *mExtent = nullptr;
187190
bool mForceRecomputeExtent = false;
188191

189192
/**
190193
* This member variable receives the same value as extent_
191194
in the method QgsOgrProvider::extent(). The purpose is to prevent a memory leak*/
192195
mutable QgsRectangle mExtentRect;
193-
OGRLayerH ogrLayer = nullptr;
194-
OGRLayerH ogrOrigLayer = nullptr;
196+
197+
//! Current working layer (might be a SQL result layer if mSubsetString is set)
198+
QgsOgrLayer *mOgrLayer = nullptr;
199+
200+
//! Original layer (not a SQL result layer)
201+
QgsOgrLayer *mOgrOrigLayer = nullptr;
195202

196203
//! path to filename
197204
QString mFilePath;
@@ -217,9 +224,6 @@ class QgsOgrProvider : public QgsVectorDataProvider
217224
//! String used to define a subset of the layer
218225
QString mSubsetString;
219226

220-
// GDAL Driver that was actually used to open the layer
221-
GDALDriverH mGDALDriver = nullptr;
222-
223227
// Friendly name of the GDAL Driver that was actually used to open the layer
224228
QString mGDALDriverName;
225229

@@ -237,8 +241,6 @@ class QgsOgrProvider : public QgsVectorDataProvider
237241
//! Calls OGR_L_SyncToDisk and recreates the spatial index if present
238242
bool syncToDisc();
239243

240-
OGRLayerH setSubsetString( OGRLayerH layer, GDALDatasetH ds );
241-
242244
friend class QgsOgrFeatureSource;
243245

244246
//! Whether the file is opened in write mode
@@ -266,9 +268,54 @@ class QgsOgrProvider : public QgsVectorDataProvider
266268
bool doInitialActionsForEdition();
267269
};
268270

269-
271+
/**
272+
\class QgsOgrProviderUtils
273+
\brief Utility class with static methods
274+
*/
270275
class QgsOgrProviderUtils
271276
{
277+
friend class QgsOgrLayer;
278+
279+
//! Identifies a dataset by name, updateMode and options
280+
class DatasetIdentification
281+
{
282+
QString toString() const;
283+
284+
public:
285+
QString dsName;
286+
bool updateMode = false;
287+
QStringList options;
288+
DatasetIdentification() {}
289+
290+
bool operator< ( const DatasetIdentification &other ) const;
291+
};
292+
293+
//! GDAL dataset objects and layers in use in it
294+
class DatasetWithLayers
295+
{
296+
public:
297+
QMutex mutex;
298+
GDALDatasetH hDS = nullptr;
299+
QMap<QString, QgsOgrLayer *> setLayers;
300+
int refCount = 0;
301+
302+
DatasetWithLayers(): mutex( QMutex::Recursive ) {}
303+
};
304+
305+
//! Global mutex for QgsOgrProviderUtils
306+
static QMutex globalMutex;
307+
308+
//! Map dataset identification to a list of corresponding DatasetWithLayers*
309+
static QMap< DatasetIdentification, QList<DatasetWithLayers *> > mapSharedDS;
310+
311+
//! Map a dataset name to the number of opened GDAL dataset objects on it (if opened with GDALOpenWrapper)
312+
static QMap< QString, int > mapCountOpenedDS;
313+
314+
//! Map a dataset name to its last modified data
315+
static QMap< QString, QDateTime > mapDSNameToLastModifiedDate;
316+
317+
static bool canUseOpenedDatasets( const QString &dsName );
318+
272319
public:
273320
static void setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes, bool firstAttrIsFid );
274321
static OGRLayerH setSubsetString( OGRLayerH layer, GDALDatasetH ds, QTextCodec *encoding, const QString &subsetString, bool &origFidAdded );
@@ -279,8 +326,220 @@ class QgsOgrProviderUtils
279326
*/
280327
static QString quotedValue( const QVariant &value );
281328

282-
static GDALDatasetH GDALOpenWrapper( const char *pszPath, bool bUpdate, bool bDisableReapck, GDALDriverH *phDriver );
329+
//! Wrapper for GDALOpenEx() that does a few lower level actions. Should be strictly paired with GDALCloseWrapper()
330+
static GDALDatasetH GDALOpenWrapper( const char *pszPath, bool bUpdate, char **papszOpenOptionsIn, GDALDriverH *phDriver );
331+
332+
//! Wrapper for GDALClose()
283333
static void GDALCloseWrapper( GDALDatasetH mhDS );
334+
335+
//! Open a layer given by name, potentially reusing an existing GDALDatasetH if it doesn't already use that layer. release() should be called when done with the object
336+
static QgsOgrLayer *getLayer( const QString &dsName,
337+
const QString &layerName,
338+
QString &errCause );
339+
340+
341+
//! Open a layer given by name, potentially reusing an existing GDALDatasetH if it has been opened with the same (updateMode, options) tuple and doesn't already use that layer. release() should be called when done with the object
342+
static QgsOgrLayer *getLayer( const QString &dsName,
343+
bool updateMode,
344+
const QStringList &options,
345+
const QString &layerName,
346+
QString &errCause );
347+
348+
//! Open a layer given by index, potentially reusing an existing GDALDatasetH if it doesn't already use that layer. release() should be called when done with the object
349+
static QgsOgrLayer *getLayer( const QString &dsName,
350+
int layerIndex,
351+
QString &errCause );
352+
353+
//! Open a layer given by index, potentially reusing an existing GDALDatasetH if it has been opened with the same (updateMode, options) tuple and doesn't already use that layer. release() should be called when done with the object
354+
static QgsOgrLayer *getLayer( const QString &dsName,
355+
bool updateMode,
356+
const QStringList &options,
357+
int layerIndex,
358+
QString &errCause );
359+
360+
//! Return a QgsOgrLayer* with a SQL result layer
361+
static QgsOgrLayer *getSqlLayer( QgsOgrLayer *baseLayer, OGRLayerH hSqlLayer, const QString &sql );
362+
363+
//! Release a QgsOgrLayer*
364+
static void release( QgsOgrLayer *&layer );
365+
366+
//! Make sure that the existing pool of opened datasets on dsName is not accessible for new getLayer() attempts
367+
static void invalidateCachedDatasets( const QString &dsName );
368+
369+
//! Return the string to provide to QgsOgrConnPool::instance() methods
370+
static QString connectionPoolId( const QString &dataSourceURI );
371+
};
372+
373+
374+
/**
375+
\class QgsOgrFeatureDefn
376+
\brief Wrap a OGRFieldDefnH object in a thread-safe way
377+
*/
378+
class QgsOgrFeatureDefn
379+
{
380+
friend class QgsOgrLayer;
381+
382+
OGRFeatureDefnH hDefn = nullptr;
383+
QgsOgrLayer *layer = nullptr;
384+
385+
QgsOgrFeatureDefn();
386+
~QgsOgrFeatureDefn();
387+
388+
OGRFeatureDefnH get();
389+
QMutex &mutex();
390+
391+
public:
392+
393+
//! Wrapper of OGR_FD_GetFieldCount
394+
int GetFieldCount();
395+
396+
//! Wrapper of OGR_FD_GetFieldDefn
397+
OGRFieldDefnH GetFieldDefn( int );
398+
399+
//! Wrapper of OGR_FD_GetFieldIndex
400+
int GetFieldIndex( const QByteArray & );
401+
402+
//! Wrapper of OGR_FD_GetGeomFieldDefn
403+
OGRGeomFieldDefnH GetGeomFieldDefn( int idx );
404+
405+
//! Wrapper of OGR_FD_GetGeomType
406+
OGRwkbGeometryType GetGeomType();
407+
408+
//! Wrapper of OGR_F_Create
409+
OGRFeatureH CreateFeature();
410+
};
411+
412+
/**
413+
\class QgsOgrLayer
414+
\brief Wrap a OGRLayerH object in a thread-safe way
415+
*/
416+
class QgsOgrLayer
417+
{
418+
friend class QgsOgrFeatureDefn;
419+
friend class QgsOgrProviderUtils;
420+
421+
QgsOgrProviderUtils::DatasetIdentification ident;
422+
bool isSqlLayer = false;
423+
QString layerName;
424+
QString sql;
425+
QgsOgrProviderUtils::DatasetWithLayers *ds = nullptr;
426+
OGRLayerH hLayer = nullptr;
427+
QgsOgrFeatureDefn oFDefn;
428+
429+
QgsOgrLayer();
430+
~QgsOgrLayer();
431+
432+
static QgsOgrLayer *CreateForLayer(
433+
const QgsOgrProviderUtils::DatasetIdentification &ident,
434+
const QString &layerName,
435+
QgsOgrProviderUtils::DatasetWithLayers *ds,
436+
OGRLayerH hLayer );
437+
438+
static QgsOgrLayer *CreateForSql(
439+
const QgsOgrProviderUtils::DatasetIdentification &ident,
440+
const QString &sql,
441+
QgsOgrProviderUtils::DatasetWithLayers *ds,
442+
OGRLayerH hLayer );
443+
444+
QMutex &mutex() { return ds->mutex; }
445+
446+
public:
447+
448+
//! Return GDALDriverH object for current dataset
449+
GDALDriverH driver();
450+
451+
//! Return driver name for current dataset
452+
QString driverName();
453+
454+
//! Return current dataset name
455+
const QString &datasetName() const { return ident.dsName; }
456+
457+
//! Return dataset open mode
458+
bool updateMode() const { return ident.updateMode; }
459+
460+
//! Return dataset open options
461+
const QStringList &options() const { return ident.options; }
462+
463+
//! Return layer name
464+
QByteArray name();
465+
466+
//! Wrapper of OGR_L_GetLayerCount
467+
int GetLayerCount();
468+
469+
//! Wrapper of OGR_L_GetLayerCount
470+
QByteArray GetFIDColumn();
471+
472+
//! Wrapper of OGR_L_GetLayerCount
473+
OGRSpatialReferenceH GetSpatialRef();
474+
475+
//! Wrapper of OGR_L_GetLayerCount
476+
void ResetReading();
477+
478+
//! Wrapper of OGR_L_GetLayerCount
479+
OGRFeatureH GetNextFeature();
480+
481+
//! Wrapper of OGR_L_GetLayerCount
482+
OGRFeatureH GetFeature( GIntBig fid );
483+
484+
//! Wrapper of OGR_L_GetLayerCount
485+
QgsOgrFeatureDefn &GetLayerDefn();
486+
487+
//! Wrapper of OGR_L_GetLayerCount
488+
GIntBig GetFeatureCount( bool force = false );
489+
490+
//! Wrapper of OGR_L_GetLayerCount
491+
OGRErr GetExtent( OGREnvelope *psExtent, bool bForce );
492+
493+
//! Wrapper of OGR_L_GetLayerCount
494+
OGRErr CreateFeature( OGRFeatureH hFeature );
495+
496+
//! Wrapper of OGR_L_GetLayerCount
497+
OGRErr SetFeature( OGRFeatureH hFeature );
498+
499+
//! Wrapper of OGR_L_GetLayerCount
500+
OGRErr DeleteFeature( GIntBig fid );
501+
502+
//! Wrapper of OGR_L_GetLayerCount
503+
OGRErr CreateField( OGRFieldDefnH hFieldDefn, bool bStrict );
504+
505+
//! Wrapper of OGR_L_GetLayerCount
506+
OGRErr DeleteField( int iField );
507+
508+
//! Wrapper of OGR_L_GetLayerCount
509+
OGRErr AlterFieldDefn( int iField, OGRFieldDefnH hNewFieldDefn, int flags );
510+
511+
//! Wrapper of OGR_L_GetLayerCount
512+
int TestCapability( const char * );
513+
514+
//! Wrapper of OGR_L_GetLayerCount
515+
OGRErr StartTransaction();
516+
517+
//! Wrapper of OGR_L_GetLayerCount
518+
OGRErr CommitTransaction();
519+
520+
//! Wrapper of OGR_L_GetLayerCount
521+
OGRErr RollbackTransaction();
522+
523+
//! Wrapper of OGR_L_GetLayerCount
524+
OGRErr SyncToDisk();
525+
526+
//! Wrapper of OGR_L_GetLayerCount
527+
OGRGeometryH GetSpatialFilter();
528+
529+
//! Wrapper of OGR_L_GetLayerCount
530+
void SetSpatialFilter( OGRGeometryH );
531+
532+
//! Return native GDALDatasetH object with the mutex to lock when using it
533+
GDALDatasetH getDatasetHandleAndMutex( QMutex *&mutex );
534+
535+
//! Return native OGRLayerH object with the mutex to lock when using it
536+
OGRLayerH getHandleAndMutex( QMutex *&mutex );
537+
538+
//! Wrapper of GDALDatasetReleaseResultSet( GDALDatasetExecuteSQL( ... ) )
539+
void ExecuteSQLNoReturn( const QByteArray &sql );
540+
541+
//! Wrapper of GDALDatasetExecuteSQL(). Returned layer must be released with QgsOgrProviderUtils::release()
542+
QgsOgrLayer *ExecuteSQL( const QByteArray &sql );
284543
};
285544

286545
// clazy:excludeall=qstring-allocations

‎tests/src/python/test_provider_ogr_gpkg.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import os
1818
import tempfile
1919
import shutil
20+
import sys
21+
import time
2022
from osgeo import gdal, ogr
2123

2224
from qgis.core import QgsVectorLayer, QgsVectorLayerExporter, QgsFeature, QgsGeometry, QgsRectangle, QgsSettings
@@ -37,6 +39,21 @@ def receiveError(self, msg):
3739
self.msg = msg
3840

3941

42+
def count_opened_filedescriptors(filename_to_test):
43+
count = -1
44+
if sys.platform.startswith('linux'):
45+
count = 0
46+
open_files_dirname = '/proc/%d/fd' % os.getpid()
47+
filenames = os.listdir(open_files_dirname)
48+
for filename in filenames:
49+
full_filename = open_files_dirname + '/' + filename
50+
if os.path.exists(full_filename):
51+
link = os.readlink(full_filename)
52+
if os.path.basename(link) == os.path.basename(filename_to_test):
53+
count += 1
54+
return count
55+
56+
4057
class TestPyQgsOGRProviderGpkg(unittest.TestCase):
4158

4259
@classmethod
@@ -513,6 +530,74 @@ def testGeopackageTwoLayerEdition(self):
513530
reference = QgsGeometry.fromWkt('Point (5 5)')
514531
self.assertEqual(got_geom.exportToWkb(), reference.exportToWkb(), 'Expected {}, got {}'.format(reference.exportToWkt(), got_geom.exportToWkt()))
515532

533+
def testGeopackageManyLayers(self):
534+
''' test opening more than 64 layers without running out of Spatialite connections '''
535+
536+
tmpfile = os.path.join(self.basetestpath, 'testGeopackageManyLayers.gpkg')
537+
ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile)
538+
for i in range(70):
539+
lyr = ds.CreateLayer('layer%d' % i, geom_type=ogr.wkbPoint)
540+
f = ogr.Feature(lyr.GetLayerDefn())
541+
f.SetGeometry(ogr.CreateGeometryFromWkt('POINT(%d 0)' % i))
542+
lyr.CreateFeature(f)
543+
f = None
544+
ds = None
545+
546+
vl_tab = []
547+
for i in range(70):
548+
layername = 'layer%d' % i
549+
vl = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + layername, layername, u'ogr')
550+
self.assertTrue(vl.isValid())
551+
vl_tab += [vl]
552+
553+
count = count_opened_filedescriptors(tmpfile)
554+
if count > 0:
555+
self.assertEqual(count, 1)
556+
557+
for i in range(70):
558+
got = [feat for feat in vl.getFeatures()]
559+
self.assertTrue(len(got) == 1)
560+
561+
# We shouldn't have more than 2 file handles opened:
562+
# one shared by the QgsOgrProvider object
563+
# one shared by the feature iterators
564+
count = count_opened_filedescriptors(tmpfile)
565+
if count > 0:
566+
self.assertEqual(count, 2)
567+
568+
# Re-open an already opened layers. We should get a new handle
569+
layername = 'layer%d' % 0
570+
vl_extra0 = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + layername, layername, u'ogr')
571+
self.assertTrue(vl_extra0.isValid())
572+
countNew = count_opened_filedescriptors(tmpfile)
573+
if countNew > 0:
574+
self.assertLessEqual(countNew, 4) # for some reason we get 4 and not 3
575+
576+
layername = 'layer%d' % 1
577+
vl_extra1 = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + layername, layername, u'ogr')
578+
self.assertTrue(vl_extra1.isValid())
579+
countNew2 = count_opened_filedescriptors(tmpfile)
580+
self.assertEqual(countNew2, countNew)
581+
582+
def testGeopackageRefreshIfTableListUpdated(self):
583+
''' test that creating/deleting a layer is reflected when opening a new layer '''
584+
585+
tmpfile = os.path.join(self.basetestpath, 'testGeopackageRefreshIfTableListUpdated.gpkg')
586+
ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile)
587+
ds.CreateLayer('test', geom_type=ogr.wkbPoint)
588+
ds = None
589+
590+
vl = QgsVectorLayer(u'{}'.format(tmpfile) + "|layername=" + "test", 'test', u'ogr')
591+
592+
time.sleep(1) # so timestamp gets updated
593+
ds = ogr.Open(tmpfile, update=1)
594+
ds.CreateLayer('test2', geom_type=ogr.wkbPoint)
595+
ds = None
596+
597+
vl2 = QgsVectorLayer(u'{}'.format(tmpfile), 'test', u'ogr')
598+
vl2.subLayers()
599+
self.assertEqual(vl2.dataProvider().subLayers(), ['0:test:0:Point:geom', '1:test2:0:Point:geom'])
600+
516601

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

0 commit comments

Comments
 (0)
Please sign in to comment.