Skip to content

Commit

Permalink
[OGR provider] Expose OGR FID as first attribute when it is meaningful
Browse files Browse the repository at this point in the history
Useful for GPKG and other database based drivers

- For OGR drivers that have GetFIDColumn() != '', expose it as a QGIS
  field.
- When creating features, use the value potentially provided in this
  first field, as the feature id to force to OGR. Disallow changing it
  in changeAttributeValues()
  • Loading branch information
rouault committed Apr 25, 2016
1 parent 0c62441 commit 8e7691a
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 23 deletions.
2 changes: 2 additions & 0 deletions src/providers/ogr/qgsogrexpressioncompiler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ QgsSqlExpressionCompiler::Result QgsOgrExpressionCompiler::compile( const QgsExp
return Fail;
else if ( mSource->mDriverName == "MSSQLSpatial" )
return Fail;
else if ( mSource->mDriverName == "GPKG" )
return Fail;

return QgsSqlExpressionCompiler::compile( exp );
}
Expand Down
14 changes: 12 additions & 2 deletions src/providers/ogr/qgsogrfeatureiterator.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ QgsOgrFeatureIterator::QgsOgrFeatureIterator( QgsOgrFeatureSource* source, bool
// filter if we choose to ignore them (fixes #11223)
if (( mSource->mDriverName != "VRT" && mSource->mDriverName != "OGR_VRT" ) || mRequest.filterRect().isNull() )
{
QgsOgrProviderUtils::setRelevantFields( ogrLayer, mSource->mFields.count(), mFetchGeometry, attrs );
QgsOgrProviderUtils::setRelevantFields( ogrLayer, mSource->mFields.count(), mFetchGeometry, attrs, mSource->mFirstFieldIsFid );
}

// spatial query to select features
Expand Down Expand Up @@ -279,8 +279,15 @@ bool QgsOgrFeatureIterator::close()

void QgsOgrFeatureIterator::getFeatureAttribute( OGRFeatureH ogrFet, QgsFeature & f, int attindex )
{
if ( mSource->mFirstFieldIsFid && attindex == 0 )
{
f.setAttribute( 0, static_cast<qint64>( OGR_F_GetFID( ogrFet ) ) );
return;
}

int attindexWithoutFid = ( mSource->mFirstFieldIsFid ) ? attindex - 1 : attindex;
bool ok = false;
QVariant value = QgsOgrUtils::getOgrFeatureAttribute( ogrFet, mSource->mFields, attindex, mSource->mEncoding, &ok );
QVariant value = QgsOgrUtils::getOgrFeatureAttribute( ogrFet, mSource->mFieldsWithoutFid, attindexWithoutFid, mSource->mEncoding, &ok );
if ( !ok )
return;

Expand Down Expand Up @@ -354,7 +361,10 @@ QgsOgrFeatureSource::QgsOgrFeatureSource( const QgsOgrProvider* p )
mSubsetString = p->mSubsetString;
mEncoding = p->mEncoding; // no copying - this is a borrowed pointer from Qt
mFields = p->mAttributeFields;
for ( int i = ( p->mFirstFieldIsFid ) ? 1 : 0; i < mFields.size(); i++ )
mFieldsWithoutFid.append( mFields.at( i ) );
mDriverName = p->ogrDriverName;
mFirstFieldIsFid = p->mFirstFieldIsFid;
mOgrGeometryTypeFilter = wkbFlatten( p->mOgrGeometryTypeFilter );
QgsOgrConnPool::instance()->ref( mFilePath );
}
Expand Down
2 changes: 2 additions & 0 deletions src/providers/ogr/qgsogrfeatureiterator.h
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ class QgsOgrFeatureSource : public QgsAbstractFeatureSource
QString mSubsetString;
QTextCodec* mEncoding;
QgsFields mFields;
bool mFirstFieldIsFid;
QgsFields mFieldsWithoutFid;
OGRwkbGeometryType mOgrGeometryTypeFilter;
QString mDriverName;

Expand Down
102 changes: 83 additions & 19 deletions src/providers/ogr/qgsogrprovider.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ QgsVectorLayerImport::ImportError QgsOgrProvider::createEmptyLayer(
QgsOgrProvider::QgsOgrProvider( QString const & uri )
: QgsVectorDataProvider( uri )
, mFirstFieldIsFid( false )
, ogrDataSource( nullptr )
, mExtent( nullptr )
, ogrLayer( nullptr )
Expand Down Expand Up @@ -733,6 +734,21 @@ void QgsOgrProvider::loadFields()
OGRFeatureDefnH fdef = OGR_L_GetLayerDefn( ogrLayer );
if ( fdef )
{
// Expose the OGR FID if it comes from a "real" column (typically GPKG)
// and make sure that this FID column is not exposed as a regular OGR field (shouldn't happen normally)
mFirstFieldIsFid = !( EQUAL( OGR_L_GetFIDColumn( ogrLayer ), "" ) ) &&
OGR_FD_GetFieldIndex( fdef, OGR_L_GetFIDColumn( ogrLayer ) ) < 0;
if ( mFirstFieldIsFid )
{
mAttributeFields.append(
QgsField(
OGR_L_GetFIDColumn( ogrLayer ),
QVariant::LongLong,
"Integer64"
)
);
}

for ( int i = 0; i < OGR_FD_GetFieldCount( fdef ); ++i )
{
OGRFieldDefnH fldDef = OGR_FD_GetFieldDefn( fdef, i );
Expand Down Expand Up @@ -817,23 +833,23 @@ QString QgsOgrProvider::storageType() const

void QgsOgrProvider::setRelevantFields( OGRLayerH ogrLayer, bool fetchGeometry, const QgsAttributeList &fetchAttributes )
{
QgsOgrProviderUtils::setRelevantFields( ogrLayer, mAttributeFields.count(), fetchGeometry, fetchAttributes );
QgsOgrProviderUtils::setRelevantFields( ogrLayer, mAttributeFields.count(), fetchGeometry, fetchAttributes, mFirstFieldIsFid );
}


void QgsOgrProviderUtils::setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes )
void QgsOgrProviderUtils::setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes, bool firstAttrIsFid )
{
#if defined(GDAL_VERSION_NUM) && GDAL_VERSION_NUM >= 1800
if ( OGR_L_TestCapability( ogrLayer, OLCIgnoreFields ) )
{
QVector<const char*> ignoredFields;
OGRFeatureDefnH featDefn = OGR_L_GetLayerDefn( ogrLayer );
for ( int i = 0; i < fieldCount; i++ )
for ( int i = ( firstAttrIsFid ? 1 : 0 ); i < fieldCount; i++ )
{
if ( !fetchAttributes.contains( i ) )
{
// add to ignored fields
ignoredFields.append( OGR_Fld_GetNameRef( OGR_FD_GetFieldDefn( featDefn, i ) ) );
ignoredFields.append( OGR_Fld_GetNameRef( OGR_FD_GetFieldDefn( featDefn, firstAttrIsFid ? i - 1 : i ) ) );
}
}

Expand Down Expand Up @@ -1025,45 +1041,65 @@ bool QgsOgrProvider::addFeature( QgsFeature& f )

QgsLocaleNumC l;

int qgisAttId = ( mFirstFieldIsFid ) ? 1 : 0;
// If the first attribute is the FID and the user has set it, then use it
if ( mFirstFieldIsFid && attrs.count() > 0 )
{
QVariant attrFid = attrs.at( 0 );
if ( !attrFid.isNull() )
{
bool ok = false;
qlonglong id = attrFid.toLongLong( &ok );
if ( ok )
{
#if GDAL_VERSION_MAJOR >= 2
OGR_F_SetFID( feature, static_cast<GIntBig>( id ) );
#else
OGR_F_SetFID( feature, static_cast<long>( id ) );
#endif
}
}
}

//add possible attribute information
for ( int targetAttributeId = 0; targetAttributeId < attrs.count(); ++targetAttributeId )
for ( int ogrAttId = 0; qgisAttId < attrs.count(); ++qgisAttId, ++ogrAttId )
{
// don't try to set field from attribute map if it's not present in layer
if ( targetAttributeId < 0 || targetAttributeId >= OGR_FD_GetFieldCount( fdef ) )
if ( ogrAttId >= OGR_FD_GetFieldCount( fdef ) )
continue;

//if(!s.isEmpty())
// continue;
//
OGRFieldDefnH fldDef = OGR_FD_GetFieldDefn( fdef, targetAttributeId );
OGRFieldDefnH fldDef = OGR_FD_GetFieldDefn( fdef, ogrAttId );
OGRFieldType type = OGR_Fld_GetType( fldDef );

QVariant attrVal = attrs.at( targetAttributeId );
QVariant attrVal = attrs.at( qgisAttId );
if ( attrVal.isNull() || ( type != OFTString && attrVal.toString().isEmpty() ) )
{
OGR_F_UnsetField( feature, targetAttributeId );
OGR_F_UnsetField( feature, ogrAttId );
}
else
{
switch ( type )
{
case OFTInteger:
OGR_F_SetFieldInteger( feature, targetAttributeId, attrVal.toInt() );
OGR_F_SetFieldInteger( feature, ogrAttId, attrVal.toInt() );
break;


#if defined(GDAL_VERSION_NUM) && GDAL_VERSION_NUM >= 2000000
case OFTInteger64:
OGR_F_SetFieldInteger64( feature, targetAttributeId, attrVal.toLongLong() );
OGR_F_SetFieldInteger64( feature, ogrAttId, attrVal.toLongLong() );
break;
#endif

case OFTReal:
OGR_F_SetFieldDouble( feature, targetAttributeId, attrVal.toDouble() );
OGR_F_SetFieldDouble( feature, ogrAttId, attrVal.toDouble() );
break;

case OFTDate:
OGR_F_SetFieldDateTime( feature, targetAttributeId,
OGR_F_SetFieldDateTime( feature, ogrAttId,
attrVal.toDate().year(),
attrVal.toDate().month(),
attrVal.toDate().day(),
Expand All @@ -1072,7 +1108,7 @@ bool QgsOgrProvider::addFeature( QgsFeature& f )
break;

case OFTTime:
OGR_F_SetFieldDateTime( feature, targetAttributeId,
OGR_F_SetFieldDateTime( feature, ogrAttId,
0, 0, 0,
attrVal.toTime().hour(),
attrVal.toTime().minute(),
Expand All @@ -1081,7 +1117,7 @@ bool QgsOgrProvider::addFeature( QgsFeature& f )
break;

case OFTDateTime:
OGR_F_SetFieldDateTime( feature, targetAttributeId,
OGR_F_SetFieldDateTime( feature, ogrAttId,
attrVal.toDateTime().date().year(),
attrVal.toDateTime().date().month(),
attrVal.toDateTime().date().day(),
Expand All @@ -1093,14 +1129,14 @@ bool QgsOgrProvider::addFeature( QgsFeature& f )

case OFTString:
QgsDebugMsg( QString( "Writing string attribute %1 with %2, encoding %3" )
.arg( targetAttributeId )
.arg( qgisAttId )
.arg( attrVal.toString(),
mEncoding->name().data() ) );
OGR_F_SetFieldString( feature, targetAttributeId, mEncoding->fromUnicode( attrVal.toString() ).constData() );
OGR_F_SetFieldString( feature, ogrAttId, mEncoding->fromUnicode( attrVal.toString() ).constData() );
break;

default:
QgsMessageLog::logMessage( tr( "type %1 for attribute %2 not found" ).arg( type ).arg( targetAttributeId ), tr( "OGR" ) );
QgsMessageLog::logMessage( tr( "type %1 for attribute %2 not found" ).arg( type ).arg( qgisAttId ), tr( "OGR" ) );
break;
}
}
Expand All @@ -1113,9 +1149,16 @@ bool QgsOgrProvider::addFeature( QgsFeature& f )
}
else
{
long id = OGR_F_GetFID( feature );
QgsFeatureId id = static_cast<QgsFeatureId>( OGR_F_GetFID( feature ) );
if ( id >= 0 )
{
f.setFeatureId( id );

if ( mFirstFieldIsFid && attrs.count() > 0 )
{
f.setAttribute( 0, id );
}
}
}
OGR_F_Destroy( feature );

Expand Down Expand Up @@ -1215,6 +1258,12 @@ bool QgsOgrProvider::deleteAttributes( const QgsAttributeIds &attributes )
qSort( attrsLst.begin(), attrsLst.end(), qGreater<int>() );
Q_FOREACH ( int attr, attrsLst )
{
if ( attr == 0 && mFirstFieldIsFid )
{
pushError( tr( "Cannot delete feature id column" ) );
res = false;
break;
}
if ( OGR_L_DeleteField( ogrLayer, attr ) != OGRERR_NONE )
{
pushError( tr( "OGR error deleting field %1: %2" ).arg( attr ).arg( CPLGetLastErrorMsg() ) );
Expand Down Expand Up @@ -1266,6 +1315,21 @@ bool QgsOgrProvider::changeAttributeValues( const QgsChangedAttributesMap &attr_
for ( QgsAttributeMap::const_iterator it2 = attr.begin(); it2 != attr.end(); ++it2 )
{
int f = it2.key();
if ( mFirstFieldIsFid )
{
if ( f == 0 )
{
if ( it2->toLongLong() != fid )
{
pushError( tr( "Changing feature id of feature %1 is not allowed." ).arg( fid ) );
continue;
}
}
else
{
--f;
}
}

OGRFieldDefnH fd = OGR_F_GetFieldDefnRef( of, f );
if ( !fd )
Expand Down
3 changes: 2 additions & 1 deletion src/providers/ogr/qgsogrprovider.h
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ class QgsOgrProvider : public QgsVectorDataProvider
QString ogrWkbGeometryTypeName( OGRwkbGeometryType type ) const;
OGRwkbGeometryType ogrWkbGeometryTypeFromName( const QString& typeName ) const;
QgsFields mAttributeFields;
bool mFirstFieldIsFid;
OGRDataSourceH ogrDataSource;
OGREnvelope* mExtent;

Expand Down Expand Up @@ -365,7 +366,7 @@ class QgsOgrProvider : public QgsVectorDataProvider
class QgsOgrProviderUtils
{
public:
static void setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes );
static void setRelevantFields( OGRLayerH ogrLayer, int fieldCount, bool fetchGeometry, const QgsAttributeList &fetchAttributes, bool firstAttrIsFid );
static OGRLayerH setSubsetString( OGRLayerH layer, OGRDataSourceH ds, QTextCodec* encoding, const QString& subsetString );
static QByteArray quotedIdentifier( QByteArray field, const QString& ogrDriverName );

Expand Down
75 changes: 74 additions & 1 deletion tests/src/python/test_provider_ogr_gpkg.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
import glob
from osgeo import gdal, ogr

from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry
from qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry, QgsFeatureRequest
from qgis.testing import start_app, unittest
from utilities import unitTestDataPath

Expand Down Expand Up @@ -81,6 +81,79 @@ def testCurveGeometryType(self):
got = [f.geometry().exportToWkt(0) for f in vl.getFeatures()][0]
self.assertEqual(got, 'CurvePolygon ((0 0, 0 1, 1 1, 0 0))')

def testFidSupport(self):

version_num = int(gdal.VersionInfo('VERSION_NUM'))
if version_num < GDAL_COMPUTE_VERSION(2, 0, 0):
return

tmpfile = os.path.join(self.basetestpath, 'testFidSupport.gpkg')
ds = ogr.GetDriverByName('GPKG').CreateDataSource(tmpfile)
lyr = ds.CreateLayer('test', geom_type=ogr.wkbPoint)
lyr.CreateField(ogr.FieldDefn('strfield', ogr.OFTString))
f = ogr.Feature(lyr.GetLayerDefn())
f.SetFID(12)
f.SetField(0, 'foo')
lyr.CreateFeature(f)
f = None
ds = None

vl = QgsVectorLayer(u'{}'.format(tmpfile), u'test', u'ogr')
self.assertEqual(len(vl.fields()), 2)
got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures()]
self.assertEqual(got, [(12, 'foo')])

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("strfield = 'foo'"))]
self.assertEqual(got, [(12, 'foo')])

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("fid = 12"))]
self.assertEqual(got, [(12, 'foo')])

result = [f['strfield'] for f in vl.dataProvider().getFeatures(QgsFeatureRequest().setSubsetOfAttributes(['strfield'], vl.dataProvider().fields()))]
self.assertEqual(result, ['foo'])

result = [f['fid'] for f in vl.dataProvider().getFeatures(QgsFeatureRequest().setSubsetOfAttributes(['fid'], vl.dataProvider().fields()))]
self.assertEqual(result, [12])

# Test that when the 'fid' field is not set, regular insertion is done
f = QgsFeature()
f.setFields(vl.fields())
f.setAttributes([None, 'automatic_id'])
(res, out_f) = vl.dataProvider().addFeatures([f])
self.assertEqual(out_f[0].id(), 13)
self.assertEqual(out_f[0].attribute('fid'), 13)
self.assertEqual(out_f[0].attribute('strfield'), 'automatic_id')

# Test that when the 'fid' field is set, it is really used to set the id
f = QgsFeature()
f.setFields(vl.fields())
f.setAttributes([9876543210, 'bar'])
(res, out_f) = vl.dataProvider().addFeatures([f])
self.assertEqual(out_f[0].id(), 9876543210)
self.assertEqual(out_f[0].attribute('fid'), 9876543210)
self.assertEqual(out_f[0].attribute('strfield'), 'bar')

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("fid = 9876543210"))]
self.assertEqual(got, [(9876543210, 'bar')])

self.assertTrue(vl.dataProvider().changeAttributeValues({9876543210: {1: 'baz'}}))

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("fid = 9876543210"))]
self.assertEqual(got, [(9876543210, 'baz')])

self.assertTrue(vl.dataProvider().changeAttributeValues({9876543210: {0: 9876543210, 1: 'baw'}}))

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("fid = 9876543210"))]
self.assertEqual(got, [(9876543210, 'baw')])

# Not allowed: changing the fid regular field
self.assertTrue(vl.dataProvider().changeAttributeValues({9876543210: {0: 12, 1: 'baw'}}))

got = [(f.attribute('fid'), f.attribute('strfield')) for f in vl.getFeatures(QgsFeatureRequest().setFilterExpression("fid = 9876543210"))]
self.assertEqual(got, [(9876543210, 'baw')])

# Cannot delete fid
self.assertFalse(vl.dataProvider().deleteAttributes([0]))

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

0 comments on commit 8e7691a

Please sign in to comment.