From 2c5a543f24864f68ff33ff2f04810ae3ed245300 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Wed, 23 Jul 2025 11:38:52 +1000 Subject: [PATCH] [mssql] Fix handling of unset attributes and constraint checks Fixes #62498 --- src/providers/mssql/qgsmssqlprovider.cpp | 14 +++ src/providers/mssql/qgsmssqlprovider.h | 1 + tests/src/python/test_provider_mssql.py | 116 +++++++++++++++++++++++ 3 files changed, 131 insertions(+) diff --git a/src/providers/mssql/qgsmssqlprovider.cpp b/src/providers/mssql/qgsmssqlprovider.cpp index ac67a73bd2c..545bcfb262d 100644 --- a/src/providers/mssql/qgsmssqlprovider.cpp +++ b/src/providers/mssql/qgsmssqlprovider.cpp @@ -443,6 +443,20 @@ QVariant QgsMssqlProvider::defaultValue( int fieldId ) const return QgsVariantUtils::isNull( res ) ? QVariant() : res; } +bool QgsMssqlProvider::skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint, const QVariant &value ) const +{ + if ( providerProperty( EvaluateDefaultValues, false ).toBool() ) + { + return !mDefaultValues.value( fieldIndex ).isEmpty(); + } + else + { + // stricter check - if we are evaluating default values only on commit then we can only bypass the check + // if the attribute values matches the original default clause + return mDefaultValues.contains( fieldIndex ) && !mDefaultValues.value( fieldIndex ).isEmpty() && ( mDefaultValues.value( fieldIndex ) == value.toString() || QgsVariantUtils::isUnsetAttributeValue( value ) ) && !QgsVariantUtils::isNull( value ); + } +} + QString QgsMssqlProvider::storageType() const { return QStringLiteral( "MSSQL spatial database" ); diff --git a/src/providers/mssql/qgsmssqlprovider.h b/src/providers/mssql/qgsmssqlprovider.h index e98722e8d35..c0cb7f20e6b 100644 --- a/src/providers/mssql/qgsmssqlprovider.h +++ b/src/providers/mssql/qgsmssqlprovider.h @@ -131,6 +131,7 @@ class QgsMssqlProvider final : public QgsVectorDataProvider QString defaultValueClause( int fieldId ) const override; QVariant defaultValue( int fieldId ) const override; + bool skipConstraintCheck( int fieldIndex, QgsFieldConstraints::Constraint constraint, const QVariant &value = QVariant() ) const override; //! Convert time value static QVariant convertTimeValue( const QVariant &value ); diff --git a/tests/src/python/test_provider_mssql.py b/tests/src/python/test_provider_mssql.py index 30eb05ed232..6ef89713f22 100644 --- a/tests/src/python/test_provider_mssql.py +++ b/tests/src/python/test_provider_mssql.py @@ -33,6 +33,8 @@ from qgis.core import ( QgsWkbTypes, QgsProviderConnectionException, QgsVectorDataProvider, + QgsUnsetAttributeValue, + QgsVectorLayerUtils, ) import unittest from qgis.testing import start_app, QgisTestCase @@ -1557,6 +1559,120 @@ class TestPyQgsMssqlProvider(QgisTestCase, MssqlProviderTestBase): ], ) + def testSkipConstraintCheck(self): + md = QgsProviderRegistry.instance().providerMetadata("mssql") + conn = md.createConnection(self.dbconn, {}) + + conn.execSql("DROP TABLE IF EXISTS qgis_test.test_constraint") + conn.execSql( + """CREATE TABLE [qgis_test].[test_constraint]( + [pk] [int] IDENTITY(1,1) NOT NULL, + [name] [nchar](10) NULL, + [geom] [geometry] NULL, + CONSTRAINT [constraint_PK_test_table] PRIMARY KEY CLUSTERED +( + [pk] ASC +)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] +) ON [PRIMARY] TEXTIMAGE_ON [PRIMARY]""" + ) + + vl = QgsVectorLayer( + '%s table="qgis_test"."test_constraint" sql=' % (self.dbconn), + "someData", + "mssql", + ) + + default_clause = "Autogenerate" + vl.dataProvider().setProviderProperty( + QgsDataProvider.ProviderProperty.EvaluateDefaultValues, False + ) + self.assertTrue( + vl.dataProvider().skipConstraintCheck( + 0, QgsFieldConstraints.Constraint.ConstraintUnique, default_clause + ) + ) + self.assertFalse( + vl.dataProvider().skipConstraintCheck( + 0, QgsFieldConstraints.Constraint.ConstraintUnique, 59 + ) + ) + self.assertTrue( + vl.dataProvider().skipConstraintCheck( + 0, + QgsFieldConstraints.Constraint.ConstraintUnique, + QgsUnsetAttributeValue(), + ) + ) + self.assertTrue( + vl.dataProvider().skipConstraintCheck( + 0, + QgsFieldConstraints.Constraint.ConstraintNotNull, + QgsUnsetAttributeValue(), + ) + ) + + def testUnsetAttributeValue(self): + """Test that QgsUnsetAttributeValue is handled correctly by the provider.""" + + self.execSQLCommand( + 'DROP TABLE IF EXISTS qgis_test."test_unset_attribute_value"' + ) + self.execSQLCommand( + 'CREATE TABLE qgis_test."test_unset_attribute_value" ([pk] [int] IDENTITY(1,1) NOT NULL, test_int SMALLINT UNIQUE NOT NULL DEFAULT 16, test_int_no_default SMALLINT NOT NULL)' + ) + + vl = QgsVectorLayer( + self.dbconn + + ' sslmode=disable table="qgis_test"."test_unset_attribute_value" sql=', + "test", + "mssql", + ) + + self.assertTrue(vl.isValid()) + + feature = QgsFeature(vl.fields()) + feature.setAttribute("test_int", QgsUnsetAttributeValue()) + feature.setAttribute("test_int_no_default", 17) + + self.assertTrue(vl.dataProvider().addFeatures([feature])) + + # Reload the layer and check the value + vl = QgsVectorLayer( + self.dbconn + + ' sslmode=disable table="qgis_test"."test_unset_attribute_value" sql=', + "test", + "mssql", + ) + self.assertTrue(vl.isValid()) + self.assertEqual(vl.featureCount(), 1) + f = next(vl.getFeatures()) + self.assertEqual(f.attribute("test_int"), 16) + self.assertEqual(f.attribute("test_int_no_default"), 17) + + self.assertFalse( + QgsVectorLayerUtils.valueExists(vl, 1, QgsUnsetAttributeValue()) + ) + self.assertTrue(QgsVectorLayerUtils.valueExists(vl, 1, 16)) + self.assertFalse(QgsVectorLayerUtils.valueExists(vl, 1, 17)) + self.assertTrue(QgsVectorLayerUtils.valueExists(vl, 2, 17)) + self.assertFalse(QgsVectorLayerUtils.valueExists(vl, 2, 16)) + self.assertTrue(QgsVectorLayerUtils.validateAttribute(vl, f, 1)[0]) + f["test_int"] = QgsUnsetAttributeValue() + self.assertTrue(QgsVectorLayerUtils.validateAttribute(vl, f, 1)[0]) + f["test_int_no_default"] = QgsUnsetAttributeValue() + + self.assertFalse( + vl.dataProvider().skipConstraintCheck( + 2, QgsFieldConstraints.Constraint.ConstraintUnique, 18 + ) + ) + self.assertFalse( + vl.dataProvider().skipConstraintCheck( + 2, QgsFieldConstraints.Constraint.ConstraintNotNull, 18 + ) + ) + self.assertFalse(QgsVectorLayerUtils.validateAttribute(vl, f, 2)[0]) + class TestPyQgsMssqlProviderQuery(QgisTestCase, MssqlProviderTestBase):