diff --git a/python/core/auto_generated/qgsvectorfilewriter.sip.in b/python/core/auto_generated/qgsvectorfilewriter.sip.in index bbb902cda54..c32a600f4e5 100644 --- a/python/core/auto_generated/qgsvectorfilewriter.sip.in +++ b/python/core/auto_generated/qgsvectorfilewriter.sip.in @@ -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 ); }; diff --git a/src/core/qgsvectorfilewriter.cpp b/src/core/qgsvectorfilewriter.cpp index 2f936c27aca..731a9ec7bac 100644 --- a/src/core/qgsvectorfilewriter.cpp +++ b/src/core/qgsvectorfilewriter.cpp @@ -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() ) ) + diff --git a/src/core/qgsvectorfilewriter.h b/src/core/qgsvectorfilewriter.h index f371814ce1e..66eebfee052 100644 --- a/src/core/qgsvectorfilewriter.h +++ b/src/core/qgsvectorfilewriter.h @@ -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 ); diff --git a/tests/src/python/test_qgsvectorfilewriter.py b/tests/src/python/test_qgsvectorfilewriter.py index 9a0ecf04315..a08489e55ef 100644 --- a/tests/src/python/test_qgsvectorfilewriter.py +++ b/tests/src/python/test_qgsvectorfilewriter.py @@ -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()