diff --git a/python/core/qgsrelation.sip b/python/core/qgsrelation.sip index ea4a1f5dc86..cc347aa2a3b 100644 --- a/python/core/qgsrelation.sip +++ b/python/core/qgsrelation.sip @@ -171,6 +171,12 @@ class QgsRelation */ QString id() const; + /** + * Generate a (project-wide) unique id for this relation + * @note added in QGIS 3.0 + */ + void generateId(); + /** * Access the referencing (child) layer's id * This is the layer which has the field(s) which point to another layer @@ -241,6 +247,15 @@ class QgsRelation */ bool isValid() const; + /** + * Compares the two QgsRelation, ignoring the name and the ID. + * + * @param other The other relation + * @return true if they are similar + * @note added in QGIS 3.0 + */ + bool hasEqualDefinition( const QgsRelation& other ) const; + protected: /** * Updates the validity status of this relation. diff --git a/python/core/qgsrelationmanager.sip b/python/core/qgsrelationmanager.sip index c77d1676659..4704694fe12 100644 --- a/python/core/qgsrelationmanager.sip +++ b/python/core/qgsrelationmanager.sip @@ -90,6 +90,16 @@ class QgsRelationManager : QObject */ QList referencedRelations( QgsVectorLayer *layer = 0 ) const; + /** + * Discover all the relations available from the current layers. + * + * @param existingRelations the existing relations to filter them out + * @param layers the current layers + * @return the list of discovered relations + * @note added in QGIS 3.0 + */ + static QList discoverRelations( const QList& existingRelations, const QList& layers ); + signals: /** This signal is emitted when the relations were loaded after reading a project */ void relationsLoaded(); diff --git a/python/core/qgsvectordataprovider.sip b/python/core/qgsvectordataprovider.sip index 90185af6601..b9c62809c02 100644 --- a/python/core/qgsvectordataprovider.sip +++ b/python/core/qgsvectordataprovider.sip @@ -371,6 +371,15 @@ class QgsVectorDataProvider : QgsDataProvider */ virtual QSet dependencies() const; + /** + * Discover the available relations with the given layers. + * @param self the layer using this data provider. + * @param layers the other layers. + * @return the list of N-1 relations from this provider. + * @note added in QGIS 3.0 + */ + virtual QList discoverRelations( const QgsVectorLayer* self, const QList& layers ) const; + signals: /** Signals an error in this provider */ void raiseError( const QString& msg ); diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index aaab88c269e..8907d907a27 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -28,6 +28,7 @@ SET(QGIS_APP_SRCS qgsdecorationscalebardialog.cpp qgsdecorationgrid.cpp qgsdecorationgriddialog.cpp + qgsdiscoverrelationsdlg.cpp qgsdxfexportdialog.cpp qgsformannotationdialog.cpp qgsguivectorlayertools.cpp @@ -207,6 +208,7 @@ SET (QGIS_APP_MOC_HDRS qgsdecorationgriddialog.h qgsdelattrdialog.h qgsdiagramproperties.h + qgsdiscoverrelationsdlg.h qgsdisplayangle.h qgsdxfexportdialog.h qgsfeatureaction.h diff --git a/src/app/qgsdiscoverrelationsdlg.cpp b/src/app/qgsdiscoverrelationsdlg.cpp new file mode 100644 index 00000000000..a524867f3f0 --- /dev/null +++ b/src/app/qgsdiscoverrelationsdlg.cpp @@ -0,0 +1,61 @@ +/*************************************************************************** + qgsdiscoverrelationsdlg.cpp + --------------------- + begin : September 2016 + copyright : (C) 2016 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#include "qgsdiscoverrelationsdlg.h" +#include "qgsvectorlayer.h" +#include "qgsrelationmanager.h" + +#include + +QgsDiscoverRelationsDlg::QgsDiscoverRelationsDlg( const QList& existingRelations, const QList& layers, QWidget *parent ) + : QDialog( parent ) + , mLayers( layers ) +{ + setupUi( this ); + + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( false ); + connect( mRelationsTable->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsDiscoverRelationsDlg::onSelectionChanged ); + + mFoundRelations = QgsRelationManager::discoverRelations( existingRelations, layers ); + Q_FOREACH ( const QgsRelation& relation, mFoundRelations ) addRelation( relation ); + + mRelationsTable->resizeColumnsToContents(); + +} + +void QgsDiscoverRelationsDlg::addRelation( const QgsRelation &rel ) +{ + const int row = mRelationsTable->rowCount(); + mRelationsTable->insertRow( row ); + mRelationsTable->setItem( row, 0, new QTableWidgetItem( rel.name() ) ); + mRelationsTable->setItem( row, 1, new QTableWidgetItem( rel.referencingLayer()->name() ) ); + mRelationsTable->setItem( row, 2, new QTableWidgetItem( rel.fieldPairs().at( 0 ).referencingField() ) ); + mRelationsTable->setItem( row, 3, new QTableWidgetItem( rel.referencedLayer()->name() ) ); + mRelationsTable->setItem( row, 4, new QTableWidgetItem( rel.fieldPairs().at( 0 ).referencedField() ) ); +} + +QList QgsDiscoverRelationsDlg::relations() const +{ + QList result; + Q_FOREACH ( const QModelIndex& row, mRelationsTable->selectionModel()->selectedRows() ) + { + result.append( mFoundRelations.at( row.row() ) ); + } + return result; +} + +void QgsDiscoverRelationsDlg::onSelectionChanged() +{ + mButtonBox->button( QDialogButtonBox::Ok )->setEnabled( mRelationsTable->selectionModel()->hasSelection() ); +} \ No newline at end of file diff --git a/src/app/qgsdiscoverrelationsdlg.h b/src/app/qgsdiscoverrelationsdlg.h new file mode 100644 index 00000000000..b890b524e12 --- /dev/null +++ b/src/app/qgsdiscoverrelationsdlg.h @@ -0,0 +1,53 @@ +/*************************************************************************** + qgsdiscoverrelationsdlg.h + --------------------- + begin : September 2016 + copyright : (C) 2016 by Patrick Valsecchi + email : patrick dot valsecchi at camptocamp dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +#ifndef QGSDISCOVERRELATIONSDLG_H +#define QGSDISCOVERRELATIONSDLG_H + +#include +#include "ui_qgsdiscoverrelationsdlgbase.h" +#include "qgsrelation.h" + +class QgsRelationManager; +class QgsVectorLayer; + +/** + * Shows the list of relations discovered from the providers. + * + * The user can select some of them to add them to his project. + */ +class APP_EXPORT QgsDiscoverRelationsDlg : public QDialog, private Ui::QgsDiscoverRelationsDlgBase +{ + Q_OBJECT + + public: + explicit QgsDiscoverRelationsDlg( const QList& existingRelations, const QList& layers, QWidget *parent = nullptr ); + + /** + * Get the selected relations. + */ + QList relations() const; + + private slots: + void onSelectionChanged(); + + private: + QList mLayers; + QList mFoundRelations; + + void addRelation( const QgsRelation &rel ); + +}; + +#endif // QGSDISCOVERRELATIONSDLG_H diff --git a/src/app/qgsrelationmanagerdialog.cpp b/src/app/qgsrelationmanagerdialog.cpp index a7c78d6b69d..04314ada37c 100644 --- a/src/app/qgsrelationmanagerdialog.cpp +++ b/src/app/qgsrelationmanagerdialog.cpp @@ -13,6 +13,7 @@ * * ***************************************************************************/ +#include "qgsdiscoverrelationsdlg.h" #include "qgsrelationadddlg.h" #include "qgsrelationmanagerdialog.h" #include "qgsrelationmanager.h" @@ -46,6 +47,7 @@ void QgsRelationManagerDialog::setLayers( const QList< QgsVectorLayer* >& layers void QgsRelationManagerDialog::addRelation( const QgsRelation &rel ) { + mRelationsTable->setSortingEnabled( false ); int row = mRelationsTable->rowCount(); mRelationsTable->insertRow( row ); @@ -54,7 +56,6 @@ void QgsRelationManagerDialog::addRelation( const QgsRelation &rel ) item->setData( Qt::UserRole, QVariant::fromValue( rel ) ); mRelationsTable->setItem( row, 0, item ); - item = new QTableWidgetItem( rel.referencingLayer()->name() ); item->setFlags( Qt::ItemIsEditable ); mRelationsTable->setItem( row, 1, item ); @@ -74,6 +75,7 @@ void QgsRelationManagerDialog::addRelation( const QgsRelation &rel ) item = new QTableWidgetItem( rel.id() ); item->setFlags( Qt::ItemIsEditable ); mRelationsTable->setItem( row, 5, item ); + mRelationsTable->setSortingEnabled( true ); } void QgsRelationManagerDialog::on_mBtnAddRelation_clicked() @@ -118,6 +120,18 @@ void QgsRelationManagerDialog::on_mBtnAddRelation_clicked() } } +void QgsRelationManagerDialog::on_mBtnDiscoverRelations_clicked() +{ + QgsDiscoverRelationsDlg discoverDlg( relations(), mLayers, this ); + if ( discoverDlg.exec() ) + { + Q_FOREACH ( const QgsRelation& relation, discoverDlg.relations() ) + { + addRelation( relation ); + } + } +} + void QgsRelationManagerDialog::on_mBtnRemoveRelation_clicked() { if ( mRelationsTable->currentIndex().isValid() ) diff --git a/src/app/qgsrelationmanagerdialog.h b/src/app/qgsrelationmanagerdialog.h index 8032beaa811..cf7ad5917f8 100644 --- a/src/app/qgsrelationmanagerdialog.h +++ b/src/app/qgsrelationmanagerdialog.h @@ -39,6 +39,7 @@ class APP_EXPORT QgsRelationManagerDialog : public QWidget, private Ui::QgsRelat public slots: void on_mBtnAddRelation_clicked(); + void on_mBtnDiscoverRelations_clicked(); void on_mBtnRemoveRelation_clicked(); private: diff --git a/src/core/qgsrelation.cpp b/src/core/qgsrelation.cpp index bdc3997d628..b2c67f8ee9e 100644 --- a/src/core/qgsrelation.cpp +++ b/src/core/qgsrelation.cpp @@ -248,6 +248,16 @@ QString QgsRelation::id() const return mRelationId; } +void QgsRelation::generateId() +{ + mRelationId = QString( "%1_%2_%3_%4" ) + .arg( referencingLayerId(), + mFieldPairs.at( 0 ).referencingField(), + referencedLayerId(), + mFieldPairs.at( 0 ).referencedField() ); + updateRelationStatus(); +} + QString QgsRelation::referencingLayerId() const { return mReferencingLayerId; @@ -301,6 +311,11 @@ bool QgsRelation::isValid() const return mValid; } +bool QgsRelation::hasEqualDefinition( const QgsRelation& other ) const +{ + return mReferencedLayerId == other.mReferencedLayerId && mReferencingLayerId == other.mReferencingLayerId && mFieldPairs == other.mFieldPairs; +} + void QgsRelation::updateRelationStatus() { const QMap& mapLayers = QgsMapLayerRegistry::instance()->mapLayers(); diff --git a/src/core/qgsrelation.h b/src/core/qgsrelation.h index 26fe3d56029..3cc48dcfe20 100644 --- a/src/core/qgsrelation.h +++ b/src/core/qgsrelation.h @@ -57,6 +57,8 @@ class CORE_EXPORT QgsRelation QString referencingField() const { return first; } //! Get the name of the referenced (parent) field QString referencedField() const { return second; } + + bool operator==( const FieldPair& other ) const { return first == other.first && second == other.second; } }; /** @@ -210,6 +212,12 @@ class CORE_EXPORT QgsRelation */ QString id() const; + /** + * Generate a (project-wide) unique id for this relation + * @note added in QGIS 3.0 + */ + void generateId(); + /** * Access the referencing (child) layer's id * This is the layer which has the field(s) which point to another layer @@ -272,6 +280,15 @@ class CORE_EXPORT QgsRelation */ bool isValid() const; + /** + * Compares the two QgsRelation, ignoring the name and the ID. + * + * @param other The other relation + * @return true if they are similar + * @note added in QGIS 3.0 + */ + bool hasEqualDefinition( const QgsRelation& other ) const; + protected: /** * Updates the validity status of this relation. diff --git a/src/core/qgsrelationmanager.cpp b/src/core/qgsrelationmanager.cpp index e077178c362..48e3d132e1c 100644 --- a/src/core/qgsrelationmanager.cpp +++ b/src/core/qgsrelationmanager.cpp @@ -19,6 +19,7 @@ #include "qgslogger.h" #include "qgsmaplayerregistry.h" #include "qgsproject.h" +#include "qgsvectordataprovider.h" #include "qgsvectorlayer.h" QgsRelationManager::QgsRelationManager( QgsProject* project ) @@ -217,3 +218,28 @@ void QgsRelationManager::layersRemoved( const QStringList& layers ) emit changed(); } } + +static bool hasRelationWithEqualDefinition( const QList& existingRelations, const QgsRelation& relation ) +{ + Q_FOREACH ( const QgsRelation& cur, existingRelations ) + { + if ( cur.hasEqualDefinition( relation ) ) return true; + } + return false; +} + +QList QgsRelationManager::discoverRelations( const QList& existingRelations, const QList& layers ) +{ + QList result; + Q_FOREACH ( const QgsVectorLayer* layer, layers ) + { + Q_FOREACH ( const QgsRelation& relation, layer->dataProvider()->discoverRelations( layer, layers ) ) + { + if ( !hasRelationWithEqualDefinition( existingRelations, relation ) ) + { + result.append( relation ); + } + } + } + return result; +} diff --git a/src/core/qgsrelationmanager.h b/src/core/qgsrelationmanager.h index ff4d3c41c27..30450c6686d 100644 --- a/src/core/qgsrelationmanager.h +++ b/src/core/qgsrelationmanager.h @@ -117,6 +117,16 @@ class CORE_EXPORT QgsRelationManager : public QObject */ QList referencedRelations( QgsVectorLayer *layer = nullptr ) const; + /** + * Discover all the relations available from the current layers. + * + * @param existingRelations the existing relations to filter them out + * @param layers the current layers + * @return the list of discovered relations + * @note added in QGIS 3.0 + */ + static QList discoverRelations( const QList& existingRelations, const QList& layers ); + signals: /** This signal is emitted when the relations were loaded after reading a project */ void relationsLoaded(); diff --git a/src/core/qgsvectordataprovider.cpp b/src/core/qgsvectordataprovider.cpp index 5a0f73229a9..b689183db89 100644 --- a/src/core/qgsvectordataprovider.cpp +++ b/src/core/qgsvectordataprovider.cpp @@ -718,3 +718,8 @@ QgsGeometry* QgsVectorDataProvider::convertToProviderType( const QgsGeometry& ge } QStringList QgsVectorDataProvider::smEncodings; + +QList QgsVectorDataProvider::discoverRelations( const QgsVectorLayer*, const QList& ) const +{ + return QList(); +} \ No newline at end of file diff --git a/src/core/qgsvectordataprovider.h b/src/core/qgsvectordataprovider.h index 7d092d5e6a7..2addd3ac4c9 100644 --- a/src/core/qgsvectordataprovider.h +++ b/src/core/qgsvectordataprovider.h @@ -28,6 +28,7 @@ class QTextCodec; #include "qgsfeature.h" #include "qgsaggregatecalculator.h" #include "qgsmaplayerdependency.h" +#include "qgsrelation.h" typedef QList QgsAttributeList; typedef QSet QgsAttributeIds; @@ -433,6 +434,15 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider */ virtual QSet dependencies() const; + /** + * Discover the available relations with the given layers. + * @param self the layer using this data provider. + * @param layers the other layers. + * @return the list of N-1 relations from this provider. + * @note added in QGIS 3.0 + */ + virtual QList discoverRelations( const QgsVectorLayer* self, const QList& layers ) const; + signals: /** Signals an error in this provider */ void raiseError( const QString& msg ); diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index 4bfcb11403f..08d862c58e1 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include @@ -3907,6 +3908,84 @@ QVariant QgsPostgresProvider::convertValue( QVariant::Type type, QVariant::Type } } +QList QgsPostgresProvider::searchLayers( const QList& layers, const QString& connectionInfo, const QString& schema, const QString& tableName ) +{ + QList result; + Q_FOREACH ( QgsVectorLayer* layer, layers ) + { + const QgsPostgresProvider* pgProvider = qobject_cast( layer->dataProvider() ); + if ( pgProvider && + pgProvider->mUri.connectionInfo( false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName ) + { + result.append( layer ); + } + } + return result; +} + +QList QgsPostgresProvider::discoverRelations( const QgsVectorLayer* self, const QList& layers ) const +{ + QList result; + QString sql( + "SELECT RC.CONSTRAINT_NAME, KCU1.COLUMN_NAME, KCU2.CONSTRAINT_SCHEMA, KCU2.TABLE_NAME, KCU2.COLUMN_NAME, KCU1.ORDINAL_POSITION " + "FROM INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS AS RC " + "INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU1 " + "ON KCU1.CONSTRAINT_CATALOG = RC.CONSTRAINT_CATALOG AND KCU1.CONSTRAINT_SCHEMA = RC.CONSTRAINT_SCHEMA AND KCU1.CONSTRAINT_NAME = RC.CONSTRAINT_NAME " + "INNER JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS KCU2 " + "ON KCU2.CONSTRAINT_CATALOG = RC.UNIQUE_CONSTRAINT_CATALOG AND KCU2.CONSTRAINT_SCHEMA = RC.UNIQUE_CONSTRAINT_SCHEMA AND KCU2.CONSTRAINT_NAME = RC.UNIQUE_CONSTRAINT_NAME " + "AND KCU2.ORDINAL_POSITION = KCU1.ORDINAL_POSITION " + "WHERE KCU1.CONSTRAINT_SCHEMA=" + QgsPostgresConn::quotedValue( mSchemaName ) + " AND KCU1.TABLE_NAME=" + QgsPostgresConn::quotedValue( mTableName ) + + "ORDER BY KCU1.ORDINAL_POSITION" + ); + QgsPostgresResult sqlResult( connectionRO()->PQexec( sql ) ); + if ( sqlResult.PQresultStatus() != PGRES_TUPLES_OK ) + { + QgsLogger::warning( "Error getting the foreign keys of " + mTableName ); + return result; + } + + int nbFound = 0; + for ( int row = 0; row < sqlResult.PQntuples(); ++row ) + { + const QString name = sqlResult.PQgetvalue( row, 0 ); + const QString fkColumn = sqlResult.PQgetvalue( row, 1 ); + const QString refSchema = sqlResult.PQgetvalue( row, 2 ); + const QString refTable = sqlResult.PQgetvalue( row, 3 ); + const QString refColumn = sqlResult.PQgetvalue( row, 4 ); + const QString position = sqlResult.PQgetvalue( row, 5 ); + if ( position == "1" ) + { // first reference field => try to find if we have layers for the referenced table + const QList foundLayers = searchLayers( layers, mUri.connectionInfo( false ), refSchema, refTable ); + Q_FOREACH ( const QgsVectorLayer* foundLayer, foundLayers ) + { + QgsRelation relation; + relation.setRelationName( name ); + relation.setReferencingLayer( self->id() ); + relation.setReferencedLayer( foundLayer->id() ); + relation.addFieldPair( fkColumn, refColumn ); + relation.generateId(); + if ( relation.isValid() ) + { + result.append( relation ); + ++nbFound; + } + else + { + QgsLogger::warning( "Invalid relation for " + name ); + } + } + } + else + { // multi reference field => add the field pair to all the referenced layers found + for ( int i = 0; i < nbFound; ++i ) + { + result[result.size() - 1 - i].addFieldPair( fkColumn, refColumn ); + } + } + } + return result; +} + /** * Class factory to return a pointer to a newly created * QgsPostgresProvider object diff --git a/src/providers/postgres/qgspostgresprovider.h b/src/providers/postgres/qgspostgresprovider.h index f604241e224..339972a3e49 100644 --- a/src/providers/postgres/qgspostgresprovider.h +++ b/src/providers/postgres/qgspostgresprovider.h @@ -258,6 +258,8 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ static QVariant convertValue( QVariant::Type type, QVariant::Type subType, const QString& value ); + virtual QList discoverRelations( const QgsVectorLayer* self, const QList& layers ) const override; + signals: /** * This is emitted whenever the worker thread has fully calculated the @@ -340,6 +342,11 @@ class QgsPostgresProvider : public QgsVectorDataProvider */ QgsPostgresPrimaryKeyType pkType( const QgsField& fld ) const; + /** + * Search all the layers using the given table. + */ + static QList searchLayers( const QList& layers, const QString& connectionInfo, const QString& schema, const QString& tableName ); + QgsFields mAttributeFields; QString mDataComment; diff --git a/src/providers/spatialite/qgsspatialiteprovider.cpp b/src/providers/spatialite/qgsspatialiteprovider.cpp index 2ceb1dcc8f8..e5e6622f718 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.cpp +++ b/src/providers/spatialite/qgsspatialiteprovider.cpp @@ -31,6 +31,7 @@ email : a.furieri@lqt.it #include "qgsspatialitefeatureiterator.h" #include +#include #include #include @@ -5264,6 +5265,80 @@ QgsAttributeList QgsSpatiaLiteProvider::pkAttributeIndexes() const return mPrimaryKeyAttrs; } +QList QgsSpatiaLiteProvider::searchLayers( const QList& layers, const QString& connectionInfo, const QString& tableName ) +{ + QList result; + Q_FOREACH ( QgsVectorLayer* layer, layers ) + { + const QgsSpatiaLiteProvider* slProvider = qobject_cast( layer->dataProvider() ); + if ( slProvider && slProvider->mSqlitePath == connectionInfo && slProvider->mTableName == tableName ) + { + result.append( layer ); + } + } + return result; +} + + +QList QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLayer* self, const QList& layers ) const +{ + QList output; + const QString sql = QString( "PRAGMA foreign_key_list(%1)" ).arg( QgsSpatiaLiteProvider::quotedIdentifier( mTableName ) ); + char **results; + int rows; + int columns; + char *errMsg = nullptr; + int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg ); + if ( ret == SQLITE_OK ) + { + int nbFound = 0; + for ( int row = 1; row <= rows; ++row ) + { + const QString name = "fk_" + mTableName + "_" + QString::fromUtf8( results[row * columns + 0] ); + const QString position = QString::fromUtf8( results[row * columns + 1] ); + const QString refTable = QString::fromUtf8( results[row * columns + 2] ); + const QString fkColumn = QString::fromUtf8( results[row * columns + 3] ); + const QString refColumn = QString::fromUtf8( results[row * columns + 4] ); + if ( position == "0" ) + { // first reference field => try to find if we have layers for the referenced table + const QList foundLayers = searchLayers( layers, mSqlitePath, refTable ); + Q_FOREACH ( const QgsVectorLayer* foundLayer, foundLayers ) + { + QgsRelation relation; + relation.setRelationName( name ); + relation.setReferencingLayer( self->id() ); + relation.setReferencedLayer( foundLayer->id() ); + relation.addFieldPair( fkColumn, refColumn ); + relation.generateId(); + if ( relation.isValid() ) + { + output.append( relation ); + ++nbFound; + } + else + { + QgsLogger::warning( "Invalid relation for " + name ); + } + } + } + else + { // multi reference field => add the field pair to all the referenced layers found + for ( int i = 0; i < nbFound; ++i ) + { + output[output.size() - 1 - i].addFieldPair( fkColumn, refColumn ); + } + } + } + sqlite3_free_table( results ); + } + else + { + QgsLogger::warning( QString( "SQLite error discovering relations: %1" ).arg( errMsg ) ); + sqlite3_free( errMsg ); + } + return output; +} + // --------------------------------------------------------------------------- QGISEXTERN bool saveStyle( const QString& uri, const QString& qmlStyle, const QString& sldStyle, diff --git a/src/providers/spatialite/qgsspatialiteprovider.h b/src/providers/spatialite/qgsspatialiteprovider.h index b38c9abe6c9..478796fcb24 100644 --- a/src/providers/spatialite/qgsspatialiteprovider.h +++ b/src/providers/spatialite/qgsspatialiteprovider.h @@ -209,6 +209,8 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider void invalidateConnections( const QString& connection ) override; + QList discoverRelations( const QgsVectorLayer* self, const QList& layers ) const override; + // static functions static void convertToGeosWKB( const unsigned char *blob, int blob_size, unsigned char **wkb, int *geom_size ); @@ -291,6 +293,11 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider //! get SpatiaLite version string QString spatialiteVersion(); + /** + * Search all the layers using the given table. + */ + static QList searchLayers( const QList& layers, const QString& connectionInfo, const QString& tableName ); + QgsFields mAttributeFields; //! Flag indicating if the layer data source is a valid SpatiaLite layer diff --git a/src/ui/qgsdiscoverrelationsdlgbase.ui b/src/ui/qgsdiscoverrelationsdlgbase.ui new file mode 100644 index 00000000000..285a15f5a8a --- /dev/null +++ b/src/ui/qgsdiscoverrelationsdlgbase.ui @@ -0,0 +1,117 @@ + + + QgsDiscoverRelationsDlgBase + + + + 0 + 0 + 700 + 267 + + + + Discover relations + + + + + + QAbstractItemView::NoEditTriggers + + + QAbstractItemView::MultiSelection + + + QAbstractItemView::SelectRows + + + true + + + true + + + false + + + true + + + + Name + + + + + Referencing Layer + + + + + Referencing Field + + + + + Referenced Layer + + + + + Referenced Field + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + mButtonBox + + + + + mButtonBox + accepted() + QgsDiscoverRelationsDlgBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + mButtonBox + rejected() + QgsDiscoverRelationsDlgBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/src/ui/qgsrelationmanagerdialogbase.ui b/src/ui/qgsrelationmanagerdialogbase.ui index f9f22c4add0..894f5af3c94 100644 --- a/src/ui/qgsrelationmanagerdialogbase.ui +++ b/src/ui/qgsrelationmanagerdialogbase.ui @@ -83,6 +83,17 @@ + + + + Discover Relations + + + + :/images/themes/default/symbologyAdd.svg:/images/themes/default/symbologyAdd.svg + + + diff --git a/tests/src/python/test_provider_spatialite.py b/tests/src/python/test_provider_spatialite.py index 78167750991..6cf54085f35 100644 --- a/tests/src/python/test_provider_spatialite.py +++ b/tests/src/python/test_provider_spatialite.py @@ -19,7 +19,7 @@ import sys import shutil import tempfile -from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry +from qgis.core import QgsVectorLayer, QgsPoint, QgsFeature, QgsGeometry, QgsProject, QgsMapLayerRegistry from qgis.testing import start_app, unittest from utilities import unitTestDataPath @@ -123,6 +123,18 @@ class TestQgsSpatialiteProvider(unittest.TestCase, ProviderTestCase): sql += "VALUES (1, '[\"toto\",\"tutu\"]', '[1,-2,724562]', '[1.0, -232567.22]', GeomFromText('POLYGON((0 0,1 0,1 1,0 1,0 0))', 4326))" cur.execute(sql) + # 2 tables with relations + sql = "PRAGMA foreign_keys = ON;" + cur.execute(sql) + sql = "CREATE TABLE test_relation_a(artistid INTEGER PRIMARY KEY, artistname TEXT);" + cur.execute(sql) + sql = "SELECT AddGeometryColumn('test_relation_a', 'Geometry', 4326, 'POLYGON', 'XY')" + cur.execute(sql) + sql = "CREATE TABLE test_relation_b(trackid INTEGER, trackname TEXT, trackartist INTEGER, FOREIGN KEY(trackartist) REFERENCES test_relation_a(artistid));" + cur.execute(sql) + sql = "SELECT AddGeometryColumn('test_relation_b', 'Geometry', 4326, 'POLYGON', 'XY')" + cur.execute(sql) + cur.execute("COMMIT") con.close() @@ -347,6 +359,29 @@ class TestQgsSpatialiteProvider(unittest.TestCase, ProviderTestCase): self.assertEqual(read_back['ints'], new_f['ints']) self.assertEqual(read_back['reals'], new_f['reals']) + def test_discover_relation(self): + artist = QgsVectorLayer("dbname=%s table=test_relation_a (geometry)" % self.dbname, "test_relation_a", "spatialite") + self.assertTrue(artist.isValid()) + track = QgsVectorLayer("dbname=%s table=test_relation_b (geometry)" % self.dbname, "test_relation_b", "spatialite") + self.assertTrue(track.isValid()) + QgsMapLayerRegistry.instance().addMapLayer(artist) + QgsMapLayerRegistry.instance().addMapLayer(track) + try: + relMgr = QgsProject.instance().relationManager() + relations = relMgr.discoverRelations([], [artist, track]) + relations = {r.name(): r for r in relations} + self.assertEqual({'fk_test_relation_b_0'}, set(relations.keys())) + + a2t = relations['fk_test_relation_b_0'] + self.assertTrue(a2t.isValid()) + self.assertEqual('test_relation_b', a2t.referencingLayer().name()) + self.assertEqual('test_relation_a', a2t.referencedLayer().name()) + self.assertEqual([2], a2t.referencingFields()) + self.assertEqual([0], a2t.referencedFields()) + finally: + QgsMapLayerRegistry.instance().removeMapLayer(track.id()) + QgsMapLayerRegistry.instance().removeMapLayer(artist.id()) + # This test would fail. It would require turning on WAL def XXXXXtestLocking(self): diff --git a/tests/src/python/test_qgsrelationeditwidget.py b/tests/src/python/test_qgsrelationeditwidget.py index 245b7345b79..b6e1c84f97f 100644 --- a/tests/src/python/test_qgsrelationeditwidget.py +++ b/tests/src/python/test_qgsrelationeditwidget.py @@ -53,15 +53,15 @@ class TestQgsRelationEditWidget(unittest.TestCase): if 'QGIS_PGTEST_DB' in os.environ: cls.dbconn = os.environ['QGIS_PGTEST_DB'] # Create test layer - cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'test', 'postgres') - cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'test', 'postgres') - cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'test', 'postgres') + cls.vl_b = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books" sql=', 'books', 'postgres') + cls.vl_a = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."authors" sql=', 'authors', 'postgres') + cls.vl_link = QgsVectorLayer(cls.dbconn + ' sslmode=disable key=\'pk\' table="qgis_test"."books_authors" sql=', 'books_authors', 'postgres') QgsMapLayerRegistry.instance().addMapLayer(cls.vl_b) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_a) QgsMapLayerRegistry.instance().addMapLayer(cls.vl_link) - relMgr = QgsProject.instance().relationManager() + cls.relMgr = QgsProject.instance().relationManager() cls.rel_a = QgsRelation() cls.rel_a.setReferencingLayer(cls.vl_link.id()) @@ -69,7 +69,7 @@ class TestQgsRelationEditWidget(unittest.TestCase): cls.rel_a.addFieldPair('fk_author', 'pk') cls.rel_a.setRelationId('rel_a') assert(cls.rel_a.isValid()) - relMgr.addRelation(cls.rel_a) + cls.relMgr.addRelation(cls.rel_a) cls.rel_b = QgsRelation() cls.rel_b.setReferencingLayer(cls.vl_link.id()) @@ -77,7 +77,7 @@ class TestQgsRelationEditWidget(unittest.TestCase): cls.rel_b.addFieldPair('fk_book', 'pk') cls.rel_b.setRelationId('rel_b') assert(cls.rel_b.isValid()) - relMgr.addRelation(cls.rel_b) + cls.relMgr.addRelation(cls.rel_b) # Our mock QgsVectorLayerTools, that allow injecting data where user input is expected cls.vltools = VlTools() @@ -125,7 +125,7 @@ class TestQgsRelationEditWidget(unittest.TestCase): self.assertEqual(self.table_view.model().rowCount(), 4) - @unittest.expectedFailure(os.environ['QT_VERSION'] == '4' and os.environ['TRAVIS_OS_NAME'] == 'linux') # It's probably not related to this variables at all, but that's the closest we can get to the real source of this problem at the moment... + @unittest.expectedFailure(os.environ.get('QT_VERSION', '5') == '4' and os.environ.get('TRAVIS_OS_NAME', '') == 'linux') # It's probably not related to this variables at all, but that's the closest we can get to the real source of this problem at the moment... def test_add_feature(self): """ Check if a new related feature is added @@ -201,6 +201,31 @@ class TestQgsRelationEditWidget(unittest.TestCase): self.assertEqual(2, self.table_view.model().rowCount()) + def test_discover_relations(self): + """ + Test the automatic discovery of relations + """ + relations = self.relMgr.discoverRelations([], [self.vl_a, self.vl_b, self.vl_link]) + relations = {r.name(): r for r in relations} + self.assertEqual({'books_authors_fk_book_fkey', 'books_authors_fk_author_fkey'}, set(relations.keys())) + + ba2b = relations['books_authors_fk_book_fkey'] + self.assertTrue(ba2b.isValid()) + self.assertEqual('books_authors', ba2b.referencingLayer().name()) + self.assertEqual('books', ba2b.referencedLayer().name()) + self.assertEqual([0], ba2b.referencingFields()) + self.assertEqual([0], ba2b.referencedFields()) + + ba2a = relations['books_authors_fk_author_fkey'] + self.assertTrue(ba2a.isValid()) + self.assertEqual('books_authors', ba2a.referencingLayer().name()) + self.assertEqual('authors', ba2a.referencedLayer().name()) + self.assertEqual([1], ba2a.referencingFields()) + self.assertEqual([0], ba2a.referencedFields()) + + self.assertEqual([], self.relMgr.discoverRelations([self.rel_a, self.rel_b], [self.vl_a, self.vl_b, self.vl_link])) + self.assertEqual(1, len(self.relMgr.discoverRelations([], [self.vl_a, self.vl_link]))) + def startTransaction(self): """ Start a new transaction and set all layers into transaction mode.