Skip to content

Commit

Permalink
Add method to fetch constraints from a vector data provider
Browse files Browse the repository at this point in the history
Implemented for unique and not null constraints for postgres
provider
  • Loading branch information
nyalldawson committed Nov 2, 2016
1 parent 5e3bef7 commit d1fd588
Show file tree
Hide file tree
Showing 7 changed files with 119 additions and 1 deletion.
19 changes: 19 additions & 0 deletions python/core/qgsvectordataprovider.sip
Expand Up @@ -55,6 +55,17 @@ class QgsVectorDataProvider : QgsDataProvider
/** Bitmask of all provider's editing capabilities */
static const int EditingCapabilities;

/**
* Constraints which may be present on a field.
* @note added in QGIS 3.0
*/
enum Constraint
{
ConstraintNotNull, //!< Field may not be null
ConstraintUnique, //!< Field must have a unique value
};
typedef QFlags<QgsVectorDataProvider::Constraint> Constraints;

/**
* Constructor of the vector provider
* @param uri uniform resource locator (URI) for a dataset
Expand Down Expand Up @@ -230,6 +241,13 @@ class QgsVectorDataProvider : QgsDataProvider
*/
virtual QVariant defaultValue( int fieldId ) const;

/**
* Returns any constraints which are present at the provider for a specified
* field index.
* @note added in QGIS 3.0
*/
virtual QgsVectorDataProvider::Constraints fieldConstraints( int fieldIndex ) const;

/**
* Changes geometries of existing features
* @param geometry_map A QgsGeometryMap whose index contains the feature IDs
Expand Down Expand Up @@ -434,3 +452,4 @@ class QgsVectorDataProvider : QgsDataProvider
};

QFlags<QgsVectorDataProvider::Capability> operator|(QgsVectorDataProvider::Capability f1, QFlags<QgsVectorDataProvider::Capability> f2);
QFlags<QgsVectorDataProvider::Constraint> operator|(QgsVectorDataProvider::Constraint f1, QFlags<QgsVectorDataProvider::Constraint> f2);
6 changes: 6 additions & 0 deletions src/core/qgsvectordataprovider.cpp
Expand Up @@ -98,6 +98,12 @@ QVariant QgsVectorDataProvider::defaultValue( int fieldId ) const
return QVariant();
}

QgsVectorDataProvider::Constraints QgsVectorDataProvider::fieldConstraints( int fieldIndex ) const
{
Q_UNUSED( fieldIndex );
return 0;
}

bool QgsVectorDataProvider::changeGeometryValues( const QgsGeometryMap &geometry_map )
{
Q_UNUSED( geometry_map );
Expand Down
19 changes: 19 additions & 0 deletions src/core/qgsvectordataprovider.h
Expand Up @@ -106,6 +106,17 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider
ChangeAttributeValues | ChangeGeometries | AddAttributes | DeleteAttributes |
RenameAttributes;

/**
* Constraints which may be present on a field.
* @note added in QGIS 3.0
*/
enum Constraint
{
ConstraintNotNull = 1, //!< Field may not be null
ConstraintUnique = 1 << 1, //!< Field must have a unique value
};
Q_DECLARE_FLAGS( Constraints, Constraint )

/**
* Constructor of the vector provider
* @param uri uniform resource locator (URI) for a dataset
Expand Down Expand Up @@ -281,6 +292,13 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider
*/
virtual QVariant defaultValue( int fieldId ) const;

/**
* Returns any constraints which are present at the provider for a specified
* field index.
* @note added in QGIS 3.0
*/
virtual Constraints fieldConstraints( int fieldIndex ) const;

/**
* Changes geometries of existing features
* @param geometry_map A QgsGeometryMap whose index contains the feature IDs
Expand Down Expand Up @@ -520,6 +538,7 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider
};

Q_DECLARE_OPERATORS_FOR_FLAGS( QgsVectorDataProvider::Capabilities )
Q_DECLARE_OPERATORS_FOR_FLAGS( QgsVectorDataProvider::Constraints )


#endif
24 changes: 23 additions & 1 deletion src/providers/postgres/qgspostgresprovider.cpp
Expand Up @@ -718,6 +718,7 @@ bool QgsPostgresProvider::loadFields()

QMap<int, QMap<int, QString> > fmtFieldTypeMap, descrMap, defValMap;
QMap<int, QMap<int, int> > attTypeIdMap;
QMap<int, QMap<int, bool> > notNullMap, uniqueMap;
if ( result.PQnfields() > 0 )
{
// Collect table oids
Expand All @@ -742,9 +743,13 @@ bool QgsPostgresProvider::loadFields()
QString tableoidsFilter = '(' + tableoidsList.join( QStringLiteral( "," ) ) + ')';

// Collect formatted field types
sql = "SELECT attrelid, attnum, pg_catalog.format_type(atttypid,atttypmod), pg_catalog.col_description(attrelid,attnum), pg_catalog.pg_get_expr(adbin,adrelid), atttypid"
sql = "SELECT attrelid, attnum, pg_catalog.format_type(atttypid,atttypmod), pg_catalog.col_description(attrelid,attnum), pg_catalog.pg_get_expr(adbin,adrelid), atttypid, attnotnull::int, indisunique::int"
" FROM pg_attribute"
" LEFT OUTER JOIN pg_attrdef ON attrelid=adrelid AND attnum=adnum"

// find unique constraints if present. Text cast required to handle int2vector comparison. Distinct required as multiple unique constraints may exist
" LEFT OUTER JOIN ( SELECT DISTINCT indrelid, indkey, indisunique FROM pg_index WHERE indisunique ) uniq ON attrelid=indrelid AND attnum::text=indkey::text "

" WHERE attrelid IN " + tableoidsFilter;
QgsPostgresResult fmtFieldTypeResult( connectionRO()->PQexec( sql ) );
for ( int i = 0; i < fmtFieldTypeResult.PQntuples(); ++i )
Expand All @@ -755,10 +760,14 @@ bool QgsPostgresProvider::loadFields()
QString descr = fmtFieldTypeResult.PQgetvalue( i, 3 );
QString defVal = fmtFieldTypeResult.PQgetvalue( i, 4 );
int attType = fmtFieldTypeResult.PQgetvalue( i, 5 ).toInt();
bool attNotNull = fmtFieldTypeResult.PQgetvalue( i, 6 ).toInt();
bool uniqueConstraint = fmtFieldTypeResult.PQgetvalue( i, 7 ).toInt();
fmtFieldTypeMap[attrelid][attnum] = formatType;
descrMap[attrelid][attnum] = descr;
defValMap[attrelid][attnum] = defVal;
attTypeIdMap[attrelid][attnum] = attType;
notNullMap[attrelid][attnum] = attNotNull;
uniqueMap[attrelid][attnum] = uniqueConstraint;
}
}
}
Expand Down Expand Up @@ -988,6 +997,14 @@ bool QgsPostgresProvider::loadFields()

mAttrPalIndexName.insert( i, fieldName );
mDefaultValues.insert( mAttributeFields.size(), defValMap[tableoid][attnum] );

Constraints constraints = 0;
if ( notNullMap[tableoid][attnum] )
constraints |= ConstraintNotNull;
if ( uniqueMap[tableoid][attnum] )
constraints |= ConstraintUnique;
mFieldConstraints.insert( mAttributeFields.size(), constraints );

mAttributeFields.append( QgsField( fieldName, fieldType, fieldTypeName, fieldSize, fieldPrec, fieldComment, fieldSubType ) );
}

Expand Down Expand Up @@ -1719,6 +1736,11 @@ QVariant QgsPostgresProvider::defaultValue( int fieldId ) const
return defVal;
}

QgsVectorDataProvider::Constraints QgsPostgresProvider::fieldConstraints( int fieldIndex ) const
{
return mFieldConstraints.value( fieldIndex, 0 );
}

QString QgsPostgresProvider::paramValue( const QString& fieldValue, const QString &defaultValue ) const
{
if ( fieldValue.isNull() )
Expand Down
2 changes: 2 additions & 0 deletions src/providers/postgres/qgspostgresprovider.h
Expand Up @@ -161,6 +161,7 @@ class QgsPostgresProvider : public QgsVectorDataProvider
QgsAttributeList attributeIndexes() const override;
QgsAttributeList pkAttributeIndexes() const override { return mPrimaryKeyAttrs; }
QVariant defaultValue( int fieldId ) const override;
Constraints fieldConstraints( int fieldIndex ) const override;

/** Adds a list of features
@return true in case of success and false in case of failure*/
Expand Down Expand Up @@ -493,6 +494,7 @@ class QgsPostgresProvider : public QgsVectorDataProvider
void setTransaction( QgsTransaction* transaction ) override;

QHash<int, QString> mDefaultValues;
QHash<int, QgsVectorDataProvider::Constraints > mFieldConstraints;
};


Expand Down
29 changes: 29 additions & 0 deletions tests/src/python/test_provider_postgres.py
Expand Up @@ -26,6 +26,7 @@
QgsFeatureRequest,
QgsFeature,
QgsTransactionGroup,
QgsVectorDataProvider,
NULL
)
from qgis.gui import QgsEditorWidgetRegistry
Expand Down Expand Up @@ -437,6 +438,34 @@ def testDoubleArray(self):
self.assertTrue(isinstance(f.attributes()[value_idx], list))
self.assertEqual(f.attributes()[value_idx], [1.1, 2, -5.12345])

def testNotNullConstraint(self):
vl = QgsVectorLayer('%s table="qgis_test"."constraints" sql=' % (self.dbconn), "constraints", "postgres")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.fields()), 4)

# test some bad field indexes
self.assertEqual(vl.dataProvider().fieldConstraints(-1), QgsVectorDataProvider.Constraints())
self.assertEqual(vl.dataProvider().fieldConstraints(1001), QgsVectorDataProvider.Constraints())

self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsVectorDataProvider.ConstraintNotNull)
self.assertFalse(vl.dataProvider().fieldConstraints(1) & QgsVectorDataProvider.ConstraintNotNull)
self.assertTrue(vl.dataProvider().fieldConstraints(2) & QgsVectorDataProvider.ConstraintNotNull)
self.assertFalse(vl.dataProvider().fieldConstraints(3) & QgsVectorDataProvider.ConstraintNotNull)

def testUniqueConstraint(self):
vl = QgsVectorLayer('%s table="qgis_test"."constraints" sql=' % (self.dbconn), "constraints", "postgres")
self.assertTrue(vl.isValid())
self.assertEqual(len(vl.fields()), 4)

# test some bad field indexes
self.assertEqual(vl.dataProvider().fieldConstraints(-1), QgsVectorDataProvider.Constraints())
self.assertEqual(vl.dataProvider().fieldConstraints(1001), QgsVectorDataProvider.Constraints())

self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsVectorDataProvider.ConstraintUnique)
self.assertTrue(vl.dataProvider().fieldConstraints(1) & QgsVectorDataProvider.ConstraintUnique)
self.assertTrue(vl.dataProvider().fieldConstraints(2) & QgsVectorDataProvider.ConstraintUnique)
self.assertFalse(vl.dataProvider().fieldConstraints(3) & QgsVectorDataProvider.ConstraintUnique)

# See http://hub.qgis.org/issues/15188
def testNumericPrecision(self):
uri = 'point?field=f1:int'
Expand Down
21 changes: 21 additions & 0 deletions tests/testdata/provider/testdata_pg.sql
Expand Up @@ -458,3 +458,24 @@ CREATE TABLE qgis_test.widget_styles(

INSERT INTO qgis_editor_widget_styles VALUES
('qgis_test', 'widget_styles', 'fld1', 'FooEdit', '<config><option key="param1" value="value1"/><option key="param2" value="2"/></config>');


-----------------------------
-- Table for constraint tests
--

DROP TABLE IF EXISTS qgis_test.constraints;
CREATE TABLE qgis_test.constraints
(
gid serial NOT NULL PRIMARY KEY, -- implicit unique key
val int, -- unique constraint
name text NOT NULL, -- unique index
description text,
CONSTRAINT constraint_val UNIQUE (val),
CONSTRAINT constraint_val2 UNIQUE (val) -- create double unique constraint for test
);

CREATE UNIQUE INDEX constraints_uniq
ON qgis_test.constraints
USING btree
(name COLLATE pg_catalog."default"); -- unique index

0 comments on commit d1fd588

Please sign in to comment.