[api] Add opt-in setting to make QgsVectorFileWriter transfer

field constraints to the output file

This is opt in to avoid potentially breaking existing scripts/plugins.
This commit is contained in:
Nyall Dawson 2023-09-11 15:30:30 +10:00
parent 8b3c8a789b
commit dce7bbdeb1
4 changed files with 154 additions and 37 deletions

View File

@ -394,6 +394,8 @@ Constructor
bool saveMetadata;
QgsLayerMetadata layerMetadata;
bool includeConstraints;
};
@ -750,6 +752,7 @@ Returns whether there are among the attributes specified some that do not exist
private:
QgsVectorFileWriter( const QgsVectorFileWriter &rh );
};

View File

@ -69,22 +69,20 @@ QgsVectorFileWriter::FieldValueConverter *QgsVectorFileWriter::FieldValueConvert
return new FieldValueConverter( *this );
}
QgsVectorFileWriter::QgsVectorFileWriter(
const QString &vectorFileName,
const QString &fileEncoding,
const QgsFields &fields,
Qgis::WkbType geometryType,
const QgsCoordinateReferenceSystem &srs,
const QString &driverName,
const QStringList &datasourceOptions,
const QStringList &layerOptions,
QString *newFilename,
Qgis::FeatureSymbologyExport symbologyExport,
QgsFeatureSink::SinkFlags sinkFlags,
QString *newLayer,
const QgsCoordinateTransformContext &transformContext,
FieldNameSource fieldNameSource
)
QgsVectorFileWriter::QgsVectorFileWriter( const QString &vectorFileName,
const QString &fileEncoding,
const QgsFields &fields,
Qgis::WkbType geometryType,
const QgsCoordinateReferenceSystem &srs,
const QString &driverName,
const QStringList &datasourceOptions,
const QStringList &layerOptions,
QString *newFilename,
Qgis::FeatureSymbologyExport symbologyExport,
QgsFeatureSink::SinkFlags sinkFlags,
QString *newLayer,
const QgsCoordinateTransformContext &transformContext,
FieldNameSource fieldNameSource )
: mError( NoError )
, mWkbType( geometryType )
, mSymbologyExport( symbologyExport )
@ -95,29 +93,29 @@ QgsVectorFileWriter::QgsVectorFileWriter(
QString(), CreateOrOverwriteFile, newLayer, sinkFlags, transformContext, fieldNameSource );
}
QgsVectorFileWriter::QgsVectorFileWriter(
const QString &vectorFileName,
const QString &fileEncoding,
const QgsFields &fields,
Qgis::WkbType geometryType,
const QgsCoordinateReferenceSystem &srs,
const QString &driverName,
const QStringList &datasourceOptions,
const QStringList &layerOptions,
QString *newFilename,
Qgis::FeatureSymbologyExport symbologyExport,
FieldValueConverter *fieldValueConverter,
const QString &layerName,
ActionOnExistingFile action,
QString *newLayer,
const QgsCoordinateTransformContext &transformContext,
QgsFeatureSink::SinkFlags sinkFlags,
FieldNameSource fieldNameSource
)
QgsVectorFileWriter::QgsVectorFileWriter( const QString &vectorFileName,
const QString &fileEncoding,
const QgsFields &fields,
Qgis::WkbType geometryType,
const QgsCoordinateReferenceSystem &srs,
const QString &driverName,
const QStringList &datasourceOptions,
const QStringList &layerOptions,
QString *newFilename,
Qgis::FeatureSymbologyExport symbologyExport,
FieldValueConverter *fieldValueConverter,
const QString &layerName,
ActionOnExistingFile action,
QString *newLayer,
const QgsCoordinateTransformContext &transformContext,
QgsFeatureSink::SinkFlags sinkFlags,
FieldNameSource fieldNameSource,
bool includeConstraints )
: mError( NoError )
, mWkbType( geometryType )
, mSymbologyExport( symbologyExport )
, mSymbologyScale( 1.0 )
, mIncludeConstraints( includeConstraints )
{
init( vectorFileName, fileEncoding, fields, geometryType, srs, driverName,
datasourceOptions, layerOptions, newFilename, fieldValueConverter,
@ -140,7 +138,7 @@ QgsVectorFileWriter *QgsVectorFileWriter::create(
return new QgsVectorFileWriter( fileName, options.fileEncoding, fields, geometryType, srs,
options.driverName, options.datasourceOptions, options.layerOptions,
newFilename, options.symbologyExport, options.fieldValueConverter, options.layerName,
options.actionOnExistingFile, newLayer, transformContext, sinkFlags, options.fieldNameSource );
options.actionOnExistingFile, newLayer, transformContext, sinkFlags, options.fieldNameSource, options.includeConstraints );
Q_NOWARN_DEPRECATED_POP
}
@ -865,6 +863,18 @@ void QgsVectorFileWriter::init( QString vectorFileName,
OGR_Fld_SetComment( fld.get(), mCodec->fromUnicode( attrField.comment() ).constData() );
#endif
if ( mIncludeConstraints )
{
if ( attrField.constraints().constraints() & QgsFieldConstraints::ConstraintNotNull )
{
OGR_Fld_SetNullable( fld.get(), false );
}
if ( attrField.constraints().constraints() & QgsFieldConstraints::ConstraintUnique )
{
OGR_Fld_SetUnique( fld.get(), true );
}
}
// create the field
QgsDebugMsgLevel( "creating field " + attrField.name() +
" type " + QString( QVariant::typeToName( attrField.type() ) ) +

View File

@ -546,6 +546,15 @@ class CORE_EXPORT QgsVectorFileWriter : public QgsFeatureSink
* \since QGIS 3.20
*/
QgsLayerMetadata layerMetadata;
/**
* Set to TRUE to transfer field constraints to the exported vector file.
*
* Support for field constraints depends on the output file format.
*
* \since QGIS 3.34
*/
bool includeConstraints = false;
};
#ifndef SIP_RUN
@ -626,6 +635,7 @@ class CORE_EXPORT QgsVectorFileWriter : public QgsFeatureSink
* \param transformContext transform context, needed if the output file srs is forced to specific crs (added in QGIS 3.10.3)
* \param sinkFlags feature sink flags (added in QGIS 3.10.3)
* \param fieldNameSource source for field names (since QGIS 3.18)
* \param includeConstraints set to TRUE to copy field constraints to the destination layer (since QGIS 3.34)
* \note not available in Python bindings
* \deprecated Use create() instead.
*/
@ -645,7 +655,8 @@ class CORE_EXPORT QgsVectorFileWriter : public QgsFeatureSink
QString *newLayer = nullptr,
const QgsCoordinateTransformContext &transformContext = QgsCoordinateTransformContext(),
QgsFeatureSink::SinkFlags sinkFlags = QgsFeatureSink::SinkFlags(),
FieldNameSource fieldNameSource = Original
FieldNameSource fieldNameSource = Original,
bool includeConstraints = false
) SIP_SKIP;
//! QgsVectorFileWriter cannot be copied.
@ -978,6 +989,9 @@ class CORE_EXPORT QgsVectorFileWriter : public QgsFeatureSink
//! Field value converter
FieldValueConverter *mFieldValueConverter = nullptr;
//! Whether to transfer field constraints to output
bool mIncludeConstraints = false;
private:
#ifdef SIP_RUN
QgsVectorFileWriter( const QgsVectorFileWriter &rh );

View File

@ -50,6 +50,7 @@ from qgis.core import (
QgsVectorFileWriter,
QgsVectorLayer,
QgsWkbTypes,
QgsFieldConstraints
)
import unittest
from qgis.testing import start_app, QgisTestCase
@ -1715,6 +1716,95 @@ class TestQgsVectorFileWriter(QgisTestCase):
self.assertFalse(
writer.capabilities() & Qgis.VectorFileWriterCapability.FieldComments)
def testWriteFieldConstraints(self):
"""
Test explicitly including field constraints.
"""
layer = QgsVectorLayer(
('Point?crs=epsg:4326&field=name:string(20)&'
'field=age:integer&field=size:double'),
'test',
'memory')
self.assertTrue(layer.isValid())
myProvider = layer.dataProvider()
layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintNotNull)
layer.setFieldConstraint(2, QgsFieldConstraints.ConstraintUnique)
ft = QgsFeature()
ft.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10, 10)))
ft.setAttributes(['Johny', 20, 0.3])
myResult, myFeatures = myProvider.addFeatures([ft])
self.assertTrue(myResult)
self.assertTrue(myFeatures)
options = QgsVectorFileWriter.SaveVectorOptions()
options.includeConstraints = True
dest = os.path.join(str(QDir.tempPath()), 'constraints.gpkg')
result, err = QgsVectorFileWriter.writeAsVectorFormatV2(
layer,
dest,
QgsProject.instance().transformContext(),
options)
self.assertEqual(result, QgsVectorFileWriter.NoError)
res = QgsVectorLayer(dest, 'result')
self.assertTrue(res.isValid())
self.assertEqual([f.name() for f in res.fields()], ['fid', 'name', 'age', 'size'])
self.assertEqual(res.fields()['name'].constraints().constraints(),
QgsFieldConstraints.Constraints())
self.assertEqual(res.fields()['age'].constraints().constraints(),
QgsFieldConstraints.ConstraintNotNull)
self.assertEqual(res.fields()['size'].constraints().constraints(),
QgsFieldConstraints.ConstraintUnique)
def testWriteSkipFieldConstraints(self):
"""
Test that default is to skip field constraints.
"""
layer = QgsVectorLayer(
('Point?crs=epsg:4326&field=name:string(20)&'
'field=age:integer&field=size:double'),
'test',
'memory')
self.assertTrue(layer.isValid())
myProvider = layer.dataProvider()
layer.setFieldConstraint(1, QgsFieldConstraints.ConstraintNotNull)
layer.setFieldConstraint(2, QgsFieldConstraints.ConstraintUnique)
ft = QgsFeature()
ft.setGeometry(QgsGeometry.fromPointXY(QgsPointXY(10, 10)))
ft.setAttributes(['Johny', 20, 0.3])
myResult, myFeatures = myProvider.addFeatures([ft])
self.assertTrue(myResult)
self.assertTrue(myFeatures)
options = QgsVectorFileWriter.SaveVectorOptions()
dest = os.path.join(str(QDir.tempPath()), 'constraints.gpkg')
result, err = QgsVectorFileWriter.writeAsVectorFormatV2(
layer,
dest,
QgsProject.instance().transformContext(),
options)
self.assertEqual(result, QgsVectorFileWriter.NoError)
res = QgsVectorLayer(dest, 'result')
self.assertTrue(res.isValid())
self.assertEqual([f.name() for f in res.fields()], ['fid', 'name', 'age', 'size'])
self.assertEqual(res.fields()['name'].constraints().constraints(),
QgsFieldConstraints.Constraints())
self.assertEqual(res.fields()['age'].constraints().constraints(),
QgsFieldConstraints.Constraints())
self.assertEqual(res.fields()['size'].constraints().constraints(),
QgsFieldConstraints.Constraints())
if __name__ == '__main__':
unittest.main()