mirror of
https://github.com/qgis/QGIS.git
synced 2025-04-14 00:07:35 -04:00
Merge pull request #3532 from pvalsecc/discover_relations
Add auto-discovery of relations for PostgresQL and SpatiaLite
This commit is contained in:
commit
702eace76c
@ -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.
|
||||
|
@ -90,6 +90,16 @@ class QgsRelationManager : QObject
|
||||
*/
|
||||
QList<QgsRelation> 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<QgsRelation> discoverRelations( const QList<QgsRelation>& existingRelations, const QList<QgsVectorLayer*>& layers );
|
||||
|
||||
signals:
|
||||
/** This signal is emitted when the relations were loaded after reading a project */
|
||||
void relationsLoaded();
|
||||
|
@ -371,6 +371,15 @@ class QgsVectorDataProvider : QgsDataProvider
|
||||
*/
|
||||
virtual QSet<QgsMapLayerDependency> 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<QgsRelation> discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& layers ) const;
|
||||
|
||||
signals:
|
||||
/** Signals an error in this provider */
|
||||
void raiseError( const QString& msg );
|
||||
|
@ -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
|
||||
|
61
src/app/qgsdiscoverrelationsdlg.cpp
Normal file
61
src/app/qgsdiscoverrelationsdlg.cpp
Normal file
@ -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 <QPushButton>
|
||||
|
||||
QgsDiscoverRelationsDlg::QgsDiscoverRelationsDlg( const QList<QgsRelation>& existingRelations, const QList<QgsVectorLayer*>& 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<QgsRelation> QgsDiscoverRelationsDlg::relations() const
|
||||
{
|
||||
QList<QgsRelation> 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() );
|
||||
}
|
53
src/app/qgsdiscoverrelationsdlg.h
Normal file
53
src/app/qgsdiscoverrelationsdlg.h
Normal file
@ -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 <QDialog>
|
||||
#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<QgsRelation>& existingRelations, const QList<QgsVectorLayer*>& layers, QWidget *parent = nullptr );
|
||||
|
||||
/**
|
||||
* Get the selected relations.
|
||||
*/
|
||||
QList<QgsRelation> relations() const;
|
||||
|
||||
private slots:
|
||||
void onSelectionChanged();
|
||||
|
||||
private:
|
||||
QList<QgsVectorLayer*> mLayers;
|
||||
QList<QgsRelation> mFoundRelations;
|
||||
|
||||
void addRelation( const QgsRelation &rel );
|
||||
|
||||
};
|
||||
|
||||
#endif // QGSDISCOVERRELATIONSDLG_H
|
@ -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<QgsRelation>( 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() )
|
||||
|
@ -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:
|
||||
|
@ -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<QString, QgsMapLayer*>& mapLayers = QgsMapLayerRegistry::instance()->mapLayers();
|
||||
|
@ -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.
|
||||
|
@ -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<QgsRelation>& existingRelations, const QgsRelation& relation )
|
||||
{
|
||||
Q_FOREACH ( const QgsRelation& cur, existingRelations )
|
||||
{
|
||||
if ( cur.hasEqualDefinition( relation ) ) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
QList<QgsRelation> QgsRelationManager::discoverRelations( const QList<QgsRelation>& existingRelations, const QList<QgsVectorLayer*>& layers )
|
||||
{
|
||||
QList<QgsRelation> 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;
|
||||
}
|
||||
|
@ -117,6 +117,16 @@ class CORE_EXPORT QgsRelationManager : public QObject
|
||||
*/
|
||||
QList<QgsRelation> 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<QgsRelation> discoverRelations( const QList<QgsRelation>& existingRelations, const QList<QgsVectorLayer*>& layers );
|
||||
|
||||
signals:
|
||||
/** This signal is emitted when the relations were loaded after reading a project */
|
||||
void relationsLoaded();
|
||||
|
@ -718,3 +718,8 @@ QgsGeometry* QgsVectorDataProvider::convertToProviderType( const QgsGeometry& ge
|
||||
}
|
||||
|
||||
QStringList QgsVectorDataProvider::smEncodings;
|
||||
|
||||
QList<QgsRelation> QgsVectorDataProvider::discoverRelations( const QgsVectorLayer*, const QList<QgsVectorLayer*>& ) const
|
||||
{
|
||||
return QList<QgsRelation>();
|
||||
}
|
@ -28,6 +28,7 @@ class QTextCodec;
|
||||
#include "qgsfeature.h"
|
||||
#include "qgsaggregatecalculator.h"
|
||||
#include "qgsmaplayerdependency.h"
|
||||
#include "qgsrelation.h"
|
||||
|
||||
typedef QList<int> QgsAttributeList;
|
||||
typedef QSet<int> QgsAttributeIds;
|
||||
@ -433,6 +434,15 @@ class CORE_EXPORT QgsVectorDataProvider : public QgsDataProvider
|
||||
*/
|
||||
virtual QSet<QgsMapLayerDependency> 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<QgsRelation> discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& layers ) const;
|
||||
|
||||
signals:
|
||||
/** Signals an error in this provider */
|
||||
void raiseError( const QString& msg );
|
||||
|
@ -24,6 +24,7 @@
|
||||
#include <qgsrectangle.h>
|
||||
#include <qgscoordinatereferencesystem.h>
|
||||
#include <qgseditformconfig.h>
|
||||
#include <qgsvectorlayer.h>
|
||||
|
||||
#include <QMessageBox>
|
||||
|
||||
@ -3907,6 +3908,84 @@ QVariant QgsPostgresProvider::convertValue( QVariant::Type type, QVariant::Type
|
||||
}
|
||||
}
|
||||
|
||||
QList<QgsVectorLayer*> QgsPostgresProvider::searchLayers( const QList<QgsVectorLayer*>& layers, const QString& connectionInfo, const QString& schema, const QString& tableName )
|
||||
{
|
||||
QList<QgsVectorLayer*> result;
|
||||
Q_FOREACH ( QgsVectorLayer* layer, layers )
|
||||
{
|
||||
const QgsPostgresProvider* pgProvider = qobject_cast<QgsPostgresProvider*>( layer->dataProvider() );
|
||||
if ( pgProvider &&
|
||||
pgProvider->mUri.connectionInfo( false ) == connectionInfo && pgProvider->mSchemaName == schema && pgProvider->mTableName == tableName )
|
||||
{
|
||||
result.append( layer );
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
QList<QgsRelation> QgsPostgresProvider::discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& layers ) const
|
||||
{
|
||||
QList<QgsRelation> 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<QgsVectorLayer*> 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
|
||||
|
@ -258,6 +258,8 @@ class QgsPostgresProvider : public QgsVectorDataProvider
|
||||
*/
|
||||
static QVariant convertValue( QVariant::Type type, QVariant::Type subType, const QString& value );
|
||||
|
||||
virtual QList<QgsRelation> discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& 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<QgsVectorLayer*> searchLayers( const QList<QgsVectorLayer*>& layers, const QString& connectionInfo, const QString& schema, const QString& tableName );
|
||||
|
||||
QgsFields mAttributeFields;
|
||||
QString mDataComment;
|
||||
|
||||
|
@ -31,6 +31,7 @@ email : a.furieri@lqt.it
|
||||
#include "qgsspatialitefeatureiterator.h"
|
||||
|
||||
#include <qgsjsonutils.h>
|
||||
#include <qgsvectorlayer.h>
|
||||
|
||||
#include <QMessageBox>
|
||||
#include <QFileInfo>
|
||||
@ -5264,6 +5265,80 @@ QgsAttributeList QgsSpatiaLiteProvider::pkAttributeIndexes() const
|
||||
return mPrimaryKeyAttrs;
|
||||
}
|
||||
|
||||
QList<QgsVectorLayer*> QgsSpatiaLiteProvider::searchLayers( const QList<QgsVectorLayer*>& layers, const QString& connectionInfo, const QString& tableName )
|
||||
{
|
||||
QList<QgsVectorLayer*> result;
|
||||
Q_FOREACH ( QgsVectorLayer* layer, layers )
|
||||
{
|
||||
const QgsSpatiaLiteProvider* slProvider = qobject_cast<QgsSpatiaLiteProvider*>( layer->dataProvider() );
|
||||
if ( slProvider && slProvider->mSqlitePath == connectionInfo && slProvider->mTableName == tableName )
|
||||
{
|
||||
result.append( layer );
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
QList<QgsRelation> QgsSpatiaLiteProvider::discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& layers ) const
|
||||
{
|
||||
QList<QgsRelation> 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<QgsVectorLayer*> 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,
|
||||
|
@ -209,6 +209,8 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider
|
||||
|
||||
void invalidateConnections( const QString& connection ) override;
|
||||
|
||||
QList<QgsRelation> discoverRelations( const QgsVectorLayer* self, const QList<QgsVectorLayer*>& 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<QgsVectorLayer*> searchLayers( const QList<QgsVectorLayer*>& layers, const QString& connectionInfo, const QString& tableName );
|
||||
|
||||
QgsFields mAttributeFields;
|
||||
|
||||
//! Flag indicating if the layer data source is a valid SpatiaLite layer
|
||||
|
117
src/ui/qgsdiscoverrelationsdlgbase.ui
Normal file
117
src/ui/qgsdiscoverrelationsdlgbase.ui
Normal file
@ -0,0 +1,117 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>QgsDiscoverRelationsDlgBase</class>
|
||||
<widget class="QDialog" name="QgsDiscoverRelationsDlgBase">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>700</width>
|
||||
<height>267</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Discover relations</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<item>
|
||||
<widget class="QTableWidget" name="mRelationsTable">
|
||||
<property name="editTriggers">
|
||||
<set>QAbstractItemView::NoEditTriggers</set>
|
||||
</property>
|
||||
<property name="selectionMode">
|
||||
<enum>QAbstractItemView::MultiSelection</enum>
|
||||
</property>
|
||||
<property name="selectionBehavior">
|
||||
<enum>QAbstractItemView::SelectRows</enum>
|
||||
</property>
|
||||
<property name="sortingEnabled">
|
||||
<bool>true</bool>
|
||||
</property>
|
||||
<attribute name="horizontalHeaderStretchLastSection">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderVisible">
|
||||
<bool>false</bool>
|
||||
</attribute>
|
||||
<attribute name="verticalHeaderShowSortIndicator" stdset="0">
|
||||
<bool>true</bool>
|
||||
</attribute>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Name</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Referencing Layer</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Referencing Field</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Referenced Layer</string>
|
||||
</property>
|
||||
</column>
|
||||
<column>
|
||||
<property name="text">
|
||||
<string>Referenced Field</string>
|
||||
</property>
|
||||
</column>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QDialogButtonBox" name="mButtonBox">
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="standardButtons">
|
||||
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>mButtonBox</tabstop>
|
||||
</tabstops>
|
||||
<resources/>
|
||||
<connections>
|
||||
<connection>
|
||||
<sender>mButtonBox</sender>
|
||||
<signal>accepted()</signal>
|
||||
<receiver>QgsDiscoverRelationsDlgBase</receiver>
|
||||
<slot>accept()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>248</x>
|
||||
<y>254</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>157</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
<connection>
|
||||
<sender>mButtonBox</sender>
|
||||
<signal>rejected()</signal>
|
||||
<receiver>QgsDiscoverRelationsDlgBase</receiver>
|
||||
<slot>reject()</slot>
|
||||
<hints>
|
||||
<hint type="sourcelabel">
|
||||
<x>316</x>
|
||||
<y>260</y>
|
||||
</hint>
|
||||
<hint type="destinationlabel">
|
||||
<x>286</x>
|
||||
<y>274</y>
|
||||
</hint>
|
||||
</hints>
|
||||
</connection>
|
||||
</connections>
|
||||
</ui>
|
@ -83,6 +83,17 @@
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mBtnDiscoverRelations">
|
||||
<property name="text">
|
||||
<string>Discover Relations</string>
|
||||
</property>
|
||||
<property name="icon">
|
||||
<iconset resource="../../images/images.qrc">
|
||||
<normaloff>:/images/themes/default/symbologyAdd.svg</normaloff>:/images/themes/default/symbologyAdd.svg</iconset>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QPushButton" name="mBtnRemoveRelation">
|
||||
<property name="text">
|
||||
|
@ -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):
|
||||
|
||||
|
@ -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.
|
||||
|
Loading…
x
Reference in New Issue
Block a user