diff --git a/src/providers/mssql/qgsmssqlprovider.cpp b/src/providers/mssql/qgsmssqlprovider.cpp index 48651feaf637..f6a1b40feed3 100644 --- a/src/providers/mssql/qgsmssqlprovider.cpp +++ b/src/providers/mssql/qgsmssqlprovider.cpp @@ -369,6 +369,22 @@ void QgsMssqlProvider::loadFields() mComputedColumns.append( query.value( 0 ).toString() ); } + // Field has unique constraint + QSet setColumnUnique; + { + if ( !query.exec( QStringLiteral( "SELECT * FROM information_schema.table_constraints TC" + " INNER JOIN information_schema.constraint_column_usage CC ON TC.Constraint_Name = CC.Constraint_Name" + " WHERE TC.CONSTRAINT_SCHEMA = '%1' AND TC.TABLE_NAME = '%2' AND TC.constraint_type = 'unique'" ) + .arg( mSchemaName, mTableName ) ) ) + { + pushError( query.lastError().text() ); + return; + } + + while ( query.next() ) + setColumnUnique.insert( query.value( QStringLiteral( "COLUMN_NAME" ) ).toString() ); + } + if ( !query.exec( QStringLiteral( "exec sp_columns @table_name = N%1, @table_owner = %2" ).arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ) ) ) { pushError( query.lastError().text() ); @@ -384,25 +400,25 @@ void QgsMssqlProvider::loadFields() // if we don't have an explicitly set geometry column name, and this is a geometry column, then use it // but if we DO have an explicitly set geometry column name, then load the other information if this is that column - if ( ( mGeometryColName.isEmpty() && ( sqlTypeName == QLatin1String( "geometry" ) || sqlTypeName == QLatin1String( "geography" ) ) ) + if ( ( mGeometryColName.isEmpty() && ( sqlTypeName == QStringLiteral( "geometry" ) || sqlTypeName == QStringLiteral( "geography" ) ) ) || colName == mGeometryColName ) { mGeometryColName = colName; mGeometryColType = sqlTypeName; - mParser.mIsGeography = sqlTypeName == QLatin1String( "geography" ); + mParser.mIsGeography = sqlTypeName == QStringLiteral( "geography" ); } else { QVariant::Type sqlType = DecodeSqlType( sqlTypeName ); - if ( sqlTypeName == QLatin1String( "int identity" ) || sqlTypeName == QLatin1String( "bigint identity" ) ) + if ( sqlTypeName == QStringLiteral( "int identity" ) || sqlTypeName == QStringLiteral( "bigint identity" ) ) { mPrimaryKeyType = PktInt; mPrimaryKeyAttrs << mAttributeFields.size(); isIdentity = true; } - else if ( sqlTypeName == QLatin1String( "int" ) || sqlTypeName == QLatin1String( "bigint" ) ) + else if ( sqlTypeName == QStringLiteral( "int" ) || sqlTypeName == QStringLiteral( "bigint" ) ) { - pkCandidates << query.value( 3 ).toString(); + pkCandidates << colName; } QgsField field; @@ -410,7 +426,7 @@ void QgsMssqlProvider::loadFields() { // Field length in chars is column 7 ("Length") of the sp_columns output, // except for uniqueidentifiers which must use column 6 ("Precision"). - int length = query.value( sqlTypeName.startsWith( QLatin1String( "uniqueidentifier" ), Qt::CaseInsensitive ) ? 6 : 7 ).toInt(); + int length = query.value( sqlTypeName.startsWith( QStringLiteral( "uniqueidentifier" ), Qt::CaseInsensitive ) ? 6 : 7 ).toInt(); if ( sqlTypeName.startsWith( QLatin1Char( 'n' ) ) ) { length = length / 2; @@ -425,8 +441,8 @@ void QgsMssqlProvider::loadFields() field = QgsField( colName, sqlType, sqlTypeName, - query.value( 6 ).toInt(), - sqlTypeName == QLatin1String( "decimal" ) ? query.value( 8 ).toInt() : -1 ); + query.value( QStringLiteral( "PRECISION" ) ).toInt(), + sqlTypeName == QStringLiteral( "decimal" ) ? query.value( QStringLiteral( "SCALE" ) ).toInt() : -1 ); } else if ( sqlType == QVariant::Date || sqlType == QVariant::DateTime || sqlType == QVariant::Time ) { @@ -445,27 +461,31 @@ void QgsMssqlProvider::loadFields() // Field nullable const bool nullable = query.value( QStringLiteral( "NULLABLE" ) ).toBool(); + + // Set constraints QgsFieldConstraints constraints; if ( !nullable ) constraints.setConstraint( QgsFieldConstraints::ConstraintNotNull, QgsFieldConstraints::ConstraintOriginProvider ); + if ( setColumnUnique.contains( colName ) ) + constraints.setConstraint( QgsFieldConstraints::ConstraintUnique, QgsFieldConstraints::ConstraintOriginProvider ); field.setConstraints( constraints ); mAttributeFields.append( field ); - //COLUMN_DEF - if ( !query.value( 12 ).isNull() ) + // Default value + if ( !query.value( QStringLiteral( "COLUMN_DEF" ) ).isNull() ) { - mDefaultValues.insert( i, query.value( 12 ).toString() ); + mDefaultValues.insert( i, query.value( QStringLiteral( "COLUMN_DEF" ) ).toString() ); } ++i; } } + // get primary key if ( mPrimaryKeyAttrs.isEmpty() ) { query.clear(); - query.setForwardOnly( true ); if ( !query.exec( QStringLiteral( "exec sp_pkeys @table_name = N%1, @table_owner = %2 " ).arg( quotedValue( mTableName ), quotedValue( mSchemaName ) ) ) ) { QgsDebugMsg( QStringLiteral( "SQL:%1\n Error:%2" ).arg( query.lastQuery(), query.lastError().text() ) ); @@ -503,7 +523,6 @@ void QgsMssqlProvider::loadFields() for ( const QString &pk : constPkCandidates ) { query.clear(); - query.setForwardOnly( true ); if ( !query.exec( QStringLiteral( "select count(distinct [%1]), count([%1]) from [%2].[%3]" ) .arg( pk, mSchemaName, mTableName ) ) ) { diff --git a/tests/src/python/test_provider_mssql.py b/tests/src/python/test_provider_mssql.py index 62efa2a201c1..7a433457a415 100644 --- a/tests/src/python/test_provider_mssql.py +++ b/tests/src/python/test_provider_mssql.py @@ -708,6 +708,42 @@ def testNotNullConstraint(self): self.assertFalse(fields.at(3).constraints().constraints() & QgsFieldConstraints.ConstraintNotNull) + def testUniqueConstraint(self): + vl = QgsVectorLayer('%s table="qgis_test"."constraints" sql=' % + (self.dbconn), "testdatetimes", "mssql") + self.assertTrue(vl.isValid()) + self.assertEqual(len(vl.fields()), 4) + + # test some bad field indexes + self.assertEqual(vl.dataProvider().fieldConstraints(-1), + QgsFieldConstraints.Constraints()) + self.assertEqual(vl.dataProvider().fieldConstraints( + 1001), QgsFieldConstraints.Constraints()) + + self.assertTrue(vl.dataProvider().fieldConstraints(0) + & QgsFieldConstraints.ConstraintUnique) + self.assertTrue(vl.dataProvider().fieldConstraints(1) + & QgsFieldConstraints.ConstraintUnique) + self.assertFalse(vl.dataProvider().fieldConstraints(2) + & QgsFieldConstraints.ConstraintUnique) + self.assertFalse(vl.dataProvider().fieldConstraints(3) + & QgsFieldConstraints.ConstraintUnique) + + # test that constraints have been saved to fields correctly + fields = vl.fields() + self.assertTrue(fields.at(0).constraints().constraints() + & QgsFieldConstraints.ConstraintUnique) + self.assertEqual(fields.at(0).constraints().constraintOrigin(QgsFieldConstraints.ConstraintUnique), + QgsFieldConstraints.ConstraintOriginProvider) + self.assertTrue(fields.at(1).constraints().constraints() + & QgsFieldConstraints.ConstraintUnique) + self.assertEqual(fields.at(1).constraints().constraintOrigin(QgsFieldConstraints.ConstraintUnique), + QgsFieldConstraints.ConstraintOriginProvider) + self.assertFalse(fields.at(2).constraints().constraints() + & QgsFieldConstraints.ConstraintUnique) + self.assertFalse(fields.at(3).constraints().constraints() + & QgsFieldConstraints.ConstraintUnique) + def getSubsetString(self): return '[cnt] > 100 and [cnt] < 410' diff --git a/tests/testdata/provider/testdata_mssql.sql b/tests/testdata/provider/testdata_mssql.sql index 826e952264b7..6b070935cf9c 100644 --- a/tests/testdata/provider/testdata_mssql.sql +++ b/tests/testdata/provider/testdata_mssql.sql @@ -295,6 +295,8 @@ CREATE TABLE [qgis_test].[constraints] gid integer PRIMARY KEY, val int, name text NOT NULL, - description text + description text, + CONSTRAINT constraint_val UNIQUE (val) ); GO +