Skip to content

Commit 3e40f80

Browse files
committed
Implement constraint detection for spatialite provider
1 parent 21f885a commit 3e40f80

File tree

4 files changed

+127
-3
lines changed

4 files changed

+127
-3
lines changed

src/gui/editorwidgets/core/qgseditorwidgetregistry.cpp

+1-2
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,7 @@ void QgsEditorWidgetRegistry::readMapLayer( QgsMapLayer* mapLayer, const QDomEle
263263

264264
formConfig.setReadOnly( idx, ewv2CfgElem.attribute( QStringLiteral( "fieldEditable" ), QStringLiteral( "1" ) ) != QLatin1String( "1" ) );
265265
formConfig.setLabelOnTop( idx, ewv2CfgElem.attribute( QStringLiteral( "labelOnTop" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) );
266-
formConfig.setNotNull( idx, ewv2CfgElem.attribute( QStringLiteral( "notNull" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) );
267-
if ( ewv2CfgElem.attribute( QStringLiteral("notNull"), QStringLiteral("0") ) == QLatin1String( "1" ) )
266+
if ( ewv2CfgElem.attribute( QStringLiteral( "notNull" ), QStringLiteral( "0" ) ) == QLatin1String( "1" ) )
268267
{
269268
// upgrade from older config
270269
vectorLayer->setFieldConstraints( idx, vectorLayer->fieldConstraints( idx ) | QgsField::ConstraintNotNull );

src/providers/spatialite/qgsspatialiteprovider.cpp

+70
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ email : a.furieri@lqt.it
3636
#include <QMessageBox>
3737
#include <QFileInfo>
3838
#include <QDir>
39+
#include <QRegularExpression>
3940

4041

4142
const QString SPATIALITE_KEY = QStringLiteral( "spatialite" );
@@ -811,6 +812,65 @@ QString QgsSpatiaLiteProvider::spatialiteVersion()
811812
return mSpatialiteVersionInfo;
812813
}
813814

815+
void QgsSpatiaLiteProvider::fetchConstraints()
816+
{
817+
char **results;
818+
char *errMsg = nullptr;
819+
820+
// this is not particularly robust but unfortunately sqlite offers no way to check directly
821+
// for the presence of constraints on a field (only indexes, but not all constraints are indexes)
822+
QString sql = QStringLiteral( "SELECT sql FROM sqlite_master WHERE type='table' AND name=%1" ).arg( quotedIdentifier( mTableName ) );
823+
int columns = 0;
824+
int rows = 0;
825+
826+
int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
827+
if ( ret != SQLITE_OK )
828+
goto error;
829+
if ( rows < 1 )
830+
;
831+
else
832+
{
833+
QString sqlDef = QString::fromUtf8( results[ 1 ] );
834+
// extract definition
835+
QRegularExpression re( QStringLiteral( "\\((.*)\\)" ) );
836+
QRegularExpressionMatch match = re.match( sqlDef );
837+
if ( match.hasMatch() )
838+
{
839+
QString matched = match.captured( 1 );
840+
Q_FOREACH ( QString field, matched.split( ',' ) )
841+
{
842+
field = field.trimmed();
843+
QString fieldName = field.left( field.indexOf( ' ' ) );
844+
QString definition = field.mid( field.indexOf( ' ' ) + 1 );
845+
QgsField::Constraints constraints = 0;
846+
if ( definition.contains( "unique", Qt::CaseInsensitive ) || definition.contains( "primary key", Qt::CaseInsensitive ) )
847+
constraints |= QgsField::ConstraintUnique;
848+
if ( definition.contains( "not null", Qt::CaseInsensitive ) || definition.contains( "primary key", Qt::CaseInsensitive ) )
849+
constraints |= QgsField::ConstraintNotNull;
850+
851+
int fieldIdx = mAttributeFields.lookupField( fieldName );
852+
if ( fieldIdx >= 0 )
853+
{
854+
mAttributeFields[ fieldIdx ].setConstraints( constraints );
855+
}
856+
}
857+
}
858+
859+
}
860+
sqlite3_free_table( results );
861+
862+
return;
863+
864+
error:
865+
QgsMessageLog::logMessage( tr( "SQLite error: %2\nSQL: %1" ).arg( sql, errMsg ? errMsg : tr( "unknown cause" ) ), tr( "SpatiaLite" ) );
866+
// unexpected error
867+
if ( errMsg )
868+
{
869+
sqlite3_free( errMsg );
870+
}
871+
872+
}
873+
814874
void QgsSpatiaLiteProvider::loadFields()
815875
{
816876
int ret;
@@ -866,6 +926,8 @@ void QgsSpatiaLiteProvider::loadFields()
866926
}
867927
sqlite3_free_table( results );
868928

929+
// check for constraints
930+
fetchConstraints();
869931

870932
// for views try to get the primary key from the meta table
871933
if ( mViewBased && mPrimaryKey.isEmpty() )
@@ -3604,6 +3666,14 @@ void QgsSpatiaLiteProvider::uniqueValues( int index, QList < QVariant > &uniqueV
36043666
return;
36053667
}
36063668

3669+
QgsField::Constraints QgsSpatiaLiteProvider::fieldConstraints( int fieldIndex ) const
3670+
{
3671+
if ( fieldIndex < 0 || fieldIndex >= mAttributeFields.count() )
3672+
return 0;
3673+
3674+
return mAttributeFields.at( fieldIndex ).constraints();
3675+
}
3676+
36073677
QString QgsSpatiaLiteProvider::geomParam() const
36083678
{
36093679
QString geometry;

src/providers/spatialite/qgsspatialiteprovider.h

+3
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider
130130
QVariant minimumValue( int index ) const override;
131131
QVariant maximumValue( int index ) const override;
132132
virtual void uniqueValues( int index, QList < QVariant > &uniqueValues, int limit = -1 ) const override;
133+
QgsField::Constraints fieldConstraints( int fieldIndex ) const override;
133134

134135
bool isValid() const override;
135136
virtual bool isSaveAndLoadStyleToDBSupported() const override { return true; }
@@ -446,6 +447,8 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider
446447
int type, int nDims, int little_endian,
447448
int endian_arch );
448449

450+
void fetchConstraints();
451+
449452
enum GEOS_3D
450453
{
451454
GEOS_3D_POINT = -2147483647,

tests/src/python/test_provider_spatialite.py

+53-1
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
import shutil
2020
import tempfile
2121

22-
from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry, QgsProject, QgsMapLayerRegistry
22+
from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry, QgsProject, QgsMapLayerRegistry, QgsField
2323

2424
from qgis.testing import start_app, unittest
2525
from utilities import unitTestDataPath
@@ -135,6 +135,10 @@ def setUpClass(cls):
135135
sql = "SELECT AddGeometryColumn('test_relation_b', 'Geometry', 4326, 'POLYGON', 'XY')"
136136
cur.execute(sql)
137137

138+
# tables with constraints
139+
sql = "CREATE TABLE test_constraints(id INTEGER PRIMARY KEY, num INTEGER NOT NULL, desc TEXT UNIQUE, desc2 TEXT, num2 INTEGER NOT NULL UNIQUE)"
140+
cur.execute(sql)
141+
138142
cur.execute("COMMIT")
139143
con.close()
140144

@@ -382,6 +386,54 @@ def test_discover_relation(self):
382386
QgsMapLayerRegistry.instance().removeMapLayer(track.id())
383387
QgsMapLayerRegistry.instance().removeMapLayer(artist.id())
384388

389+
def testNotNullConstraint(self):
390+
vl = QgsVectorLayer("dbname=%s table=test_constraints key='id'" % self.dbname, "test_constraints",
391+
"spatialite")
392+
self.assertTrue(vl.isValid())
393+
self.assertEqual(len(vl.fields()), 5)
394+
395+
# test some bad field indexes
396+
self.assertEqual(vl.dataProvider().fieldConstraints(-1), QgsField.Constraints())
397+
self.assertEqual(vl.dataProvider().fieldConstraints(1001), QgsField.Constraints())
398+
399+
self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsField.ConstraintNotNull)
400+
self.assertTrue(vl.dataProvider().fieldConstraints(1) & QgsField.ConstraintNotNull)
401+
self.assertFalse(vl.dataProvider().fieldConstraints(2) & QgsField.ConstraintNotNull)
402+
self.assertFalse(vl.dataProvider().fieldConstraints(3) & QgsField.ConstraintNotNull)
403+
self.assertTrue(vl.dataProvider().fieldConstraints(4) & QgsField.ConstraintNotNull)
404+
405+
# test that constraints have been saved to fields correctly
406+
fields = vl.fields()
407+
self.assertTrue(fields.at(0).constraints() & QgsField.ConstraintNotNull)
408+
self.assertTrue(fields.at(1).constraints() & QgsField.ConstraintNotNull)
409+
self.assertFalse(fields.at(2).constraints() & QgsField.ConstraintNotNull)
410+
self.assertFalse(fields.at(3).constraints() & QgsField.ConstraintNotNull)
411+
self.assertTrue(fields.at(4).constraints() & QgsField.ConstraintNotNull)
412+
413+
def testUniqueConstraint(self):
414+
vl = QgsVectorLayer("dbname=%s table=test_constraints key='id'" % self.dbname, "test_constraints",
415+
"spatialite")
416+
self.assertTrue(vl.isValid())
417+
self.assertEqual(len(vl.fields()), 5)
418+
419+
# test some bad field indexes
420+
self.assertEqual(vl.dataProvider().fieldConstraints(-1), QgsField.Constraints())
421+
self.assertEqual(vl.dataProvider().fieldConstraints(1001), QgsField.Constraints())
422+
423+
self.assertTrue(vl.dataProvider().fieldConstraints(0) & QgsField.ConstraintUnique)
424+
self.assertFalse(vl.dataProvider().fieldConstraints(1) & QgsField.ConstraintUnique)
425+
self.assertTrue(vl.dataProvider().fieldConstraints(2) & QgsField.ConstraintUnique)
426+
self.assertFalse(vl.dataProvider().fieldConstraints(3) & QgsField.ConstraintUnique)
427+
self.assertTrue(vl.dataProvider().fieldConstraints(4) & QgsField.ConstraintUnique)
428+
429+
# test that constraints have been saved to fields correctly
430+
fields = vl.fields()
431+
self.assertTrue(fields.at(0).constraints() & QgsField.ConstraintUnique)
432+
self.assertFalse(fields.at(1).constraints() & QgsField.ConstraintUnique)
433+
self.assertTrue(fields.at(2).constraints() & QgsField.ConstraintUnique)
434+
self.assertFalse(fields.at(3).constraints() & QgsField.ConstraintUnique)
435+
self.assertTrue(fields.at(4).constraints() & QgsField.ConstraintUnique)
436+
385437
# This test would fail. It would require turning on WAL
386438
def XXXXXtestLocking(self):
387439

0 commit comments

Comments
 (0)