diff --git a/src/providers/postgres/qgspostgresprovider.cpp b/src/providers/postgres/qgspostgresprovider.cpp index 48aaa85b8140..f86ddac87b09 100644 --- a/src/providers/postgres/qgspostgresprovider.cpp +++ b/src/providers/postgres/qgspostgresprovider.cpp @@ -70,6 +70,12 @@ static bool tableExists( QgsPostgresConn &conn, const QString &name ) return res.PQgetvalue( 0, 0 ).startsWith( 't' ); } +static bool columnExists( QgsPostgresConn &conn, const QString &table, const QString &column ) +{ + QgsPostgresResult res( conn.PQexec( "SELECT COUNT(*) FROM information_schema.columns WHERE table_name=" + QgsPostgresConn::quotedValue( table ) + " and column_name=" + QgsPostgresConn::quotedValue( column ) ) ); + return res.PQgetvalue( 0, 0 ).toInt() > 0; +} + QgsPostgresPrimaryKeyType QgsPostgresProvider::pkType( const QgsField &f ) const { @@ -4877,26 +4883,43 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & return false; } - QgsPostgresResult res( conn->PQexec( "CREATE TABLE IF NOT EXISTS layer_styles(" - "id SERIAL PRIMARY KEY" - ",f_table_catalog varchar" - ",f_table_schema varchar" - ",f_table_name varchar" - ",f_geometry_column varchar" - ",styleName text" - ",styleQML xml" - ",styleSLD xml" - ",useAsDefault boolean" - ",description text" - ",owner varchar(63) DEFAULT CURRENT_USER" - ",ui xml" - ",update_time timestamp DEFAULT CURRENT_TIMESTAMP" - ")" ) ); - if ( res.PQresultStatus() != PGRES_COMMAND_OK ) - { - errCause = QObject::tr( "Unable to save layer style. It's not possible to create the destination table on the database. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() ); - conn->unref(); - return false; + if ( !tableExists( *conn, QStringLiteral( "layer_styles" ) ) ) + { + QgsPostgresResult res( conn->PQexec( "CREATE TABLE layer_styles(" + "id SERIAL PRIMARY KEY" + ",f_table_catalog varchar" + ",f_table_schema varchar" + ",f_table_name varchar" + ",f_geometry_column varchar" + ",styleName text" + ",styleQML xml" + ",styleSLD xml" + ",useAsDefault boolean" + ",description text" + ",owner varchar(63) DEFAULT CURRENT_USER" + ",ui xml" + ",update_time timestamp DEFAULT CURRENT_TIMESTAMP" + ",type varchar" + ")" ) ); + if ( res.PQresultStatus() != PGRES_COMMAND_OK ) + { + errCause = QObject::tr( "Unable to save layer style. It's not possible to create the destination table on the database. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() ); + conn->unref(); + return false; + } + } + else + { + if ( !columnExists( *conn, QStringLiteral( "layer_styles" ), QStringLiteral( "type" ) ) ) + { + QgsPostgresResult res( conn->PQexec( "ALTER TABLE layer_styles ADD COLUMN type varchar NULL" ) ); + if ( res.PQresultStatus() != PGRES_COMMAND_OK ) + { + errCause = QObject::tr( "Unable to add column type to layer_styles table. Maybe this is due to table permissions (user=%1). Please contact your database admin" ).arg( dsUri.username() ); + conn->unref(); + return false; + } + } } if ( dsUri.database().isEmpty() ) // typically when a service file is used @@ -4912,15 +4935,17 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & uiFileValue = QStringLiteral( ",XMLPARSE(DOCUMENT %1)" ).arg( QgsPostgresConn::quotedValue( uiFileContent ) ); } + const QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) ); + // Note: in the construction of the INSERT and UPDATE strings the qmlStyle and sldStyle values // can contain user entered strings, which may themselves include %## values that would be // replaced by the QString.arg function. To ensure that the final SQL string is not corrupt these // two values are both replaced in the final .arg call of the string construction. QString sql = QString( "INSERT INTO layer_styles(" - "f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner%11" + "f_table_catalog,f_table_schema,f_table_name,f_geometry_column,styleName,styleQML,styleSLD,useAsDefault,description,owner,type%12" ") VALUES (" - "%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10%12" + "%1,%2,%3,%4,%5,XMLPARSE(DOCUMENT %16),XMLPARSE(DOCUMENT %17),%8,%9,%10,%11%13" ")" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) @@ -4932,6 +4957,7 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & .arg( "CURRENT_USER" ) .arg( uiFileColumn ) .arg( uiFileValue ) + .arg( wkbTypeString ) // Must be the final .arg replacement - see above .arg( QgsPostgresConn::quotedValue( qmlStyle ), QgsPostgresConn::quotedValue( sldStyle ) ); @@ -4942,14 +4968,16 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column=%4" - " AND styleName=%5" ) + " AND type=%5" + " AND styleName=%6" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( wkbTypeString ) .arg( QgsPostgresConn::quotedValue( styleName.isEmpty() ? dsUri.table() : styleName ) ); - res = conn->PQexec( checkQuery ); + QgsPostgresResult res( conn->PQexec( checkQuery ) ); if ( res.PQntuples() > 0 ) { if ( QMessageBox::question( nullptr, QObject::tr( "Save style in database" ), @@ -4972,8 +5000,10 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & " AND f_table_schema=%7" " AND f_table_name=%8" " AND f_geometry_column=%9" - " AND styleName=%10" ) + " AND styleName=%10" + " AND type=%2" ) .arg( useAsDefault ? "true" : "false" ) + .arg( wkbTypeString ) .arg( QgsPostgresConn::quotedValue( styleDescription.isEmpty() ? QDateTime::currentDateTime().toString() : styleDescription ) ) .arg( "CURRENT_USER" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) @@ -4993,11 +5023,14 @@ bool QgsPostgresProviderMetadata::saveStyle( const QString &uri, const QString & " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" - " AND f_geometry_column=%4" ) + " AND f_geometry_column=%4" + " AND type=%5" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( wkbTypeString ); + sql = QStringLiteral( "BEGIN; %1; %2; COMMIT;" ).arg( removeDefaultSql, sql ); } @@ -5045,18 +5078,22 @@ QString QgsPostgresProviderMetadata::loadStyle( const QString &uri, QString &err geomColumnExpr = QStringLiteral( "=" ) + QgsPostgresConn::quotedValue( dsUri.geometryColumn() ); } + QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) ); + QString selectQmlQuery = QString( "SELECT styleQML" " FROM layer_styles" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column %4" + " AND type=%5" " ORDER BY CASE WHEN useAsDefault THEN 1 ELSE 2 END" ",update_time DESC LIMIT 1" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( geomColumnExpr ); + .arg( geomColumnExpr ) + .arg( wkbTypeString ); QgsPostgresResult result( conn->PQexec( selectQmlQuery ) ); @@ -5083,17 +5120,21 @@ int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &id dsUri.setDatabase( conn->currentDatabase() ); } + QString wkbTypeString = QgsPostgresConn::quotedValue( QgsWkbTypes::geometryDisplayString( QgsWkbTypes::geometryType( dsUri.wkbType() ) ) ); + QString selectRelatedQuery = QString( "SELECT id,styleName,description" " FROM layer_styles" " WHERE f_table_catalog=%1" " AND f_table_schema=%2" " AND f_table_name=%3" " AND f_geometry_column=%4" + " AND type=%5" " ORDER BY useasdefault DESC, update_time DESC" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( wkbTypeString ); QgsPostgresResult result( conn->PQexec( selectRelatedQuery ) ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) @@ -5114,12 +5155,13 @@ int QgsPostgresProviderMetadata::listStyles( const QString &uri, QStringList &id QString selectOthersQuery = QString( "SELECT id,styleName,description" " FROM layer_styles" - " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4)" + " WHERE NOT (f_table_catalog=%1 AND f_table_schema=%2 AND f_table_name=%3 AND f_geometry_column=%4 AND type=%5)" " ORDER BY update_time DESC" ) .arg( QgsPostgresConn::quotedValue( dsUri.database() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.schema() ) ) .arg( QgsPostgresConn::quotedValue( dsUri.table() ) ) - .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ); + .arg( QgsPostgresConn::quotedValue( dsUri.geometryColumn() ) ) + .arg( wkbTypeString ); result = conn->PQexec( selectOthersQuery ); if ( result.PQresultStatus() != PGRES_TUPLES_OK ) diff --git a/tests/src/python/test_provider_postgres.py b/tests/src/python/test_provider_postgres.py index 85194c1a0f70..6efbc53da20c 100644 --- a/tests/src/python/test_provider_postgres.py +++ b/tests/src/python/test_provider_postgres.py @@ -1265,6 +1265,45 @@ def testStyle(self): self.assertEqual(desclist, []) self.assertEqual(errmsg, "") + def testStyleWithGeometryType(self): + """Test saving styles with the additional geometry type + Layers are created from geometries_table + """ + + myconn = 'service=\'qgis_test\'' + if 'QGIS_PGTEST_DB' in os.environ: + myconn = os.environ['QGIS_PGTEST_DB'] + + # point layer + myPoint = QgsVectorLayer(myconn + ' sslmode=disable srid=4326 type=POINT table="qgis_test"."geometries_table" (geom) sql=', 'Point', 'postgres') + self.assertTrue(myPoint.isValid()) + myPoint.saveStyleToDatabase('myPointStyle', '', False, '') + + # polygon layer + myPolygon = QgsVectorLayer(myconn + ' sslmode=disable srid=4326 type=POLYGON table="qgis_test"."geometries_table" (geom) sql=', 'Poly', 'postgres') + self.assertTrue(myPoint.isValid()) + myPolygon.saveStyleToDatabase('myPolygonStyle', '', False, '') + + # how many + related_count, idlist, namelist, desclist, errmsg = myPolygon.listStylesInDatabase() + self.assertEqual(len(idlist), 2) + self.assertEqual(namelist, ['myPolygonStyle', 'myPointStyle']) + + # raw psycopg2 query + self.assertTrue(self.con) + cur = self.con.cursor() + self.assertTrue(cur) + cur.execute("select stylename, type from layer_styles order by type") + self.assertEqual(cur.fetchall(), [('myPointStyle', 'Point'), ('myPolygonStyle', 'Polygon')]) + cur.close() + + # delete them + myPolygon.deleteStyleFromDatabase(idlist[1]) + myPolygon.deleteStyleFromDatabase(idlist[0]) + styles = myPolygon.listStylesInDatabase() + ids = styles[1] + self.assertEqual(len(ids), 0) + def testHasMetadata(self): # views don't have metadata vl = QgsVectorLayer('{} table="qgis_test"."{}" key="pk" sql='.format(self.dbconn, 'bikes_view'), "bikes_view", "postgres")