Skip to content

Commit 381627e

Browse files
authored
Merge pull request #8714 from elpaso/release-3_4-backports
Release 3.4 backports (spatialite exotic queries and views)
2 parents e16472a + 9b8fd50 commit 381627e

File tree

5 files changed

+192
-45
lines changed

5 files changed

+192
-45
lines changed

python/plugins/db_manager/dlg_sql_window.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,8 @@ def _getSqlLayer(self, _filter):
388388
if layer.isValid():
389389
return layer
390390
else:
391+
e = BaseError(self.tr("There was an error creating the SQL layer, please check the logs for further information."))
392+
DlgDbError.showError(e, self)
391393
return None
392394

393395
def loadSqlLayer(self):

src/providers/spatialite/qgsspatialiteprovider.cpp

Lines changed: 123 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,34 @@ void QgsSpatiaLiteProvider::determineViewPrimaryKey()
11231123
}
11241124
}
11251125

1126+
QStringList QgsSpatiaLiteProvider::tablePrimaryKeys( const QString &tableName ) const
1127+
{
1128+
QList<QString> result;
1129+
const QString sql = QStringLiteral( "PRAGMA table_info(%1)" ).arg( QgsSpatiaLiteProvider::quotedIdentifier( tableName ) );
1130+
char **results = nullptr;
1131+
int rows;
1132+
int columns;
1133+
char *errMsg = nullptr;
1134+
int ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
1135+
if ( ret == SQLITE_OK )
1136+
{
1137+
for ( int row = 1; row <= rows; ++row )
1138+
{
1139+
if ( QString::fromUtf8( results[row * columns + 5] ) == QChar( '1' ) )
1140+
{
1141+
result << QString::fromUtf8( results[row * columns + 1] );
1142+
}
1143+
}
1144+
sqlite3_free_table( results );
1145+
}
1146+
else
1147+
{
1148+
QgsLogger::warning( QStringLiteral( "SQLite error discovering primary keys: %1" ).arg( errMsg ) );
1149+
sqlite3_free( errMsg );
1150+
}
1151+
return result;
1152+
}
1153+
11261154

11271155
bool QgsSpatiaLiteProvider::hasTriggers()
11281156
{
@@ -4557,8 +4585,6 @@ bool QgsSpatiaLiteProvider::checkLayerType()
45574585
}
45584586
else if ( mQuery.startsWith( '(' ) && mQuery.endsWith( ')' ) )
45594587
{
4560-
// checking if this one is a select query
4561-
45624588
// get a new alias for the subquery
45634589
int index = 0;
45644590
QString alias;
@@ -4579,61 +4605,127 @@ bool QgsSpatiaLiteProvider::checkLayerType()
45794605

45804606
sql = QStringLiteral( "SELECT 0, %1 FROM %2 LIMIT 1" ).arg( quotedIdentifier( mGeometryColumn ), mQuery );
45814607
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4608+
4609+
// Try to find a PK or try to use ROWID
45824610
if ( ret == SQLITE_OK && rows == 1 )
45834611
{
4584-
// Check if we can get use the ROWID from the table that provides the geometry
45854612
sqlite3_stmt *stmt = nullptr;
4586-
//! String containing the name of the table that provides the geometry if the layer data source is based on a query
4587-
QString queryGeomTableName;
4613+
45884614
// 1. find the table that provides geometry
4615+
// String containing the name of the table that provides the geometry if the layer data source is based on a query
4616+
QString queryGeomTableName;
45894617
if ( sqlite3_prepare_v2( mSqliteHandle, sql.toUtf8().constData(), -1, &stmt, nullptr ) == SQLITE_OK )
45904618
{
45914619
queryGeomTableName = sqlite3_column_table_name( stmt, 1 );
45924620
}
4593-
// 2. check if the table has a usable ROWID
4621+
4622+
// 3. Find pks
4623+
QList<QString> pks;
45944624
if ( ! queryGeomTableName.isEmpty() )
45954625
{
4596-
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( quotedIdentifier( queryGeomTableName ) );
4597-
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4598-
if ( ret != SQLITE_OK || rows != 1 )
4599-
{
4600-
queryGeomTableName = QString();
4601-
}
4626+
pks = tablePrimaryKeys( queryGeomTableName );
46024627
}
4603-
// 3. check if ROWID injection works
4628+
4629+
// find table alias if any
4630+
QString tableAlias;
46044631
if ( ! queryGeomTableName.isEmpty() )
46054632
{
4606-
// Check if the whole sql is aliased (I couldn't find a sqlite API call to get this information)
4607-
QRegularExpression re { R"re(\s+AS\s+(\w+)\n?\)?$)re" };
4633+
// Try first with single table alias
4634+
// (I couldn't find a sqlite API call to get this information)
4635+
QRegularExpression re { QStringLiteral( R"re("?%1"?\s+AS\s+(\w+))re" ).arg( queryGeomTableName ) };
46084636
re.setPatternOptions( QRegularExpression::PatternOption::MultilineOption |
46094637
QRegularExpression::PatternOption::CaseInsensitiveOption );
46104638
QRegularExpressionMatch match { re.match( mTableName ) };
4611-
regex.setPattern( QStringLiteral( R"re(\s+AS\s+(\w+)\n?\)?$)re" ) );
4612-
QString tableAlias;
46134639
if ( match.hasMatch() )
46144640
{
46154641
tableAlias = match.captured( 1 );
46164642
}
4617-
QString newSql( mQuery.replace( QStringLiteral( "SELECT " ),
4618-
QStringLiteral( "SELECT %1.%2, " )
4619-
.arg( quotedIdentifier( tableAlias.isEmpty() ? queryGeomTableName : tableAlias ),
4620-
QStringLiteral( "ROWID" ) ),
4621-
Qt::CaseInsensitive ) );
4622-
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( newSql );
4643+
// Check if the whole sql is aliased i.e. '(SELECT * FROM \\"somedata\\" as my_alias\n)'
4644+
if ( tableAlias.isEmpty() )
4645+
{
4646+
regex.setPattern( QStringLiteral( R"re(\s+AS\s+(\w+)\n?\)?$)re" ) );
4647+
match = re.match( mTableName );
4648+
if ( match.hasMatch() )
4649+
{
4650+
tableAlias = match.captured( 1 );
4651+
}
4652+
}
4653+
}
4654+
4655+
const QString tableIdentifier { tableAlias.isEmpty() ? queryGeomTableName : tableAlias };
4656+
QRegularExpression injectionRe { QStringLiteral( R"re(SELECT\s([^\(]+?FROM\s+"?%1"?))re" ).arg( tableIdentifier ) };
4657+
injectionRe.setPatternOptions( QRegularExpression::PatternOption::MultilineOption |
4658+
QRegularExpression::PatternOption::CaseInsensitiveOption );
4659+
4660+
4661+
if ( ! pks.isEmpty() )
4662+
{
4663+
if ( pks.length() > 1 )
4664+
{
4665+
QgsMessageLog::logMessage( tr( "SQLite composite keys are not supported in query layer, using the first component only. %1" )
4666+
.arg( sql ), tr( "SpatiaLite" ), Qgis::MessageLevel::Warning );
4667+
}
4668+
4669+
// Try first without any injection or manipulation
4670+
sql = QStringLiteral( "SELECT %1, %2 FROM %3 LIMIT 1" ).arg( quotedIdentifier( pks.first( ) ), quotedIdentifier( mGeometryColumn ), mQuery );
46234671
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
46244672
if ( ret == SQLITE_OK && rows == 1 )
46254673
{
4626-
mQuery = newSql;
4627-
mPrimaryKey = QStringLiteral( "ROWID" );
4628-
mRowidInjectedInQuery = true;
4674+
mPrimaryKey = pks.first( );
4675+
}
4676+
else // if that does not work, try injection with table name/alias
4677+
{
4678+
QString pk { QStringLiteral( "%1.%2" ).arg( quotedIdentifier( alias ) ).arg( pks.first() ) };
4679+
QString newSql( mQuery.replace( injectionRe,
4680+
QStringLiteral( R"re(SELECT %1.%2, \1)re" )
4681+
.arg( quotedIdentifier( tableIdentifier ) )
4682+
.arg( pks.first() ) ) );
4683+
sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 1" ).arg( pk ).arg( newSql );
4684+
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4685+
if ( ret == SQLITE_OK && rows == 1 )
4686+
{
4687+
mQuery = newSql;
4688+
mPrimaryKey = pks.first( );
4689+
}
46294690
}
46304691
}
4631-
// 4. if it does not work, simply clear the message and fallback to the original behavior
4632-
if ( errMsg )
4692+
4693+
// If there is still no primary key, check if we can get use the ROWID from the table that provides the geometry
4694+
if ( mPrimaryKey.isEmpty() )
46334695
{
4634-
QgsMessageLog::logMessage( tr( "SQLite error while trying to inject ROWID: %2\nSQL: %1" ).arg( sql, errMsg ), tr( "SpatiaLite" ) );
4635-
sqlite3_free( errMsg );
4636-
errMsg = nullptr;
4696+
// 4. check if the table has a usable ROWID
4697+
if ( ! queryGeomTableName.isEmpty() )
4698+
{
4699+
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( quotedIdentifier( queryGeomTableName ) );
4700+
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4701+
if ( ret != SQLITE_OK || rows != 1 )
4702+
{
4703+
queryGeomTableName = QString();
4704+
}
4705+
}
4706+
// 5. check if ROWID injection works
4707+
if ( ! queryGeomTableName.isEmpty() )
4708+
{
4709+
const QString newSql( mQuery.replace( injectionRe,
4710+
QStringLiteral( R"re(SELECT %1.%2, \1)re" )
4711+
.arg( quotedIdentifier( tableIdentifier ),
4712+
QStringLiteral( "ROWID" ) ) ) );
4713+
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( newSql );
4714+
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4715+
if ( ret == SQLITE_OK && rows == 1 )
4716+
{
4717+
mQuery = newSql;
4718+
mPrimaryKey = QStringLiteral( "ROWID" );
4719+
mRowidInjectedInQuery = true;
4720+
}
4721+
}
4722+
// 6. if it does not work, simply clear the message and fallback to the original behavior
4723+
if ( errMsg )
4724+
{
4725+
QgsMessageLog::logMessage( tr( "SQLite error while trying to inject ROWID: %2\nSQL: %1" ).arg( sql, errMsg ), tr( "SpatiaLite" ) );
4726+
sqlite3_free( errMsg );
4727+
errMsg = nullptr;
4728+
}
46374729
}
46384730
sqlite3_finalize( stmt );
46394731
mIsQuery = true;

src/providers/spatialite/qgsspatialiteprovider.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,9 @@ class QgsSpatiaLiteProvider: public QgsVectorDataProvider
202202
//! For views, try to get primary key from a dedicated meta table
203203
void determineViewPrimaryKey();
204204

205+
//! Returns primary key(s) from a table name
206+
QStringList tablePrimaryKeys( const QString &tableName ) const;
207+
205208
//! Check if a table/view has any triggers. Triggers can be used on views to make them editable.
206209
bool hasTriggers();
207210

tests/src/python/test_provider_spatialite.py

Lines changed: 64 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,36 @@ def testLoadStyle(self):
789789
err, ok = vl.loadDefaultStyle()
790790
self.assertTrue(ok)
791791

792+
def _aliased_sql_helper(self, dbname):
793+
queries = (
794+
'(SELECT * FROM (SELECT * from \\"some view\\"))',
795+
'(SELECT * FROM \\"some view\\")',
796+
'(select sd.* from somedata as sd left join somedata as sd2 on ( sd2.name = sd.name ))',
797+
'(select sd.* from \\"somedata\\" as sd left join \\"somedata\\" as sd2 on ( sd2.name = sd.name ))',
798+
"(SELECT * FROM somedata as my_alias1\n)",
799+
"(SELECT * FROM somedata as my_alias2)",
800+
"(SELECT * FROM somedata AS my_alias3)",
801+
'(SELECT * FROM \\"somedata\\" as my_alias4\n)',
802+
'(SELECT * FROM (SELECT * FROM \\"somedata\\"))',
803+
'(SELECT my_alias5.* FROM (SELECT * FROM \\"somedata\\") AS my_alias5)',
804+
'(SELECT my_alias6.* FROM (SELECT * FROM \\"somedata\\" as my_alias\n) AS my_alias6)',
805+
'(SELECT my_alias7.* FROM (SELECT * FROM \\"somedata\\" as my_alias\n) AS my_alias7\n)',
806+
'(SELECT my_alias8.* FROM (SELECT * FROM \\"some data\\") AS my_alias8)',
807+
'(SELECT my_alias9.* FROM (SELECT * FROM \\"some data\\" as my_alias\n) AS my_alias9)',
808+
'(SELECT my_alias10.* FROM (SELECT * FROM \\"some data\\" as my_alias\n) AS my_alias10\n)',
809+
'(select sd.* from \\"some data\\" as sd left join \\"some data\\" as sd2 on ( sd2.name = sd.name ))',
810+
'(SELECT * FROM \\"some data\\" as my_alias11\n)',
811+
'(SELECT * FROM \\"some data\\" as my_alias12)',
812+
'(SELECT * FROM \\"some data\\" AS my_alias13)',
813+
'(SELECT * from \\"some data\\" AS my_alias14\n)',
814+
'(SELECT * FROM (SELECT * from \\"some data\\"))',
815+
)
816+
for sql in queries:
817+
vl = QgsVectorLayer('dbname=\'{}\' table="{}" (geom) sql='.format(dbname, sql), 'test', 'spatialite')
818+
self.assertTrue(vl.isValid(), 'dbname: {} - sql: {}'.format(dbname, sql))
819+
self.assertTrue(vl.featureCount() > 1)
820+
self.assertTrue(vl.isSpatial())
821+
792822
def testPkLessQuery(self):
793823
"""Test if features in queries with/without pk can be retrieved by id"""
794824
# create test db
@@ -802,29 +832,36 @@ def testPkLessQuery(self):
802832
cur.execute(sql)
803833

804834
# simple table with primary key
805-
sql = "CREATE TABLE test_pk (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL)"
835+
sql = "CREATE TABLE \"test pk\" (id INTEGER NOT NULL PRIMARY KEY, name TEXT NOT NULL)"
806836
cur.execute(sql)
807837

808-
sql = "SELECT AddGeometryColumn('test_pk', 'geometry', 4326, 'POINT', 'XY')"
838+
sql = "SELECT AddGeometryColumn('test pk', 'geometry', 4326, 'POINT', 'XY')"
809839
cur.execute(sql)
810840

811841
for i in range(11, 21):
812-
sql = "INSERT INTO test_pk (id, name, geometry) "
842+
sql = "INSERT INTO \"test pk\" (id, name, geometry) "
813843
sql += "VALUES ({id}, 'name {id}', GeomFromText('POINT({id} {id})', 4326))".format(id=i)
814844
cur.execute(sql)
815845

816-
# simple table without primary key
817-
sql = "CREATE TABLE test_no_pk (name TEXT NOT NULL)"
818-
cur.execute(sql)
819-
820-
sql = "SELECT AddGeometryColumn('test_no_pk', 'geometry', 4326, 'POINT', 'XY')"
821-
cur.execute(sql)
846+
def _make_table(table_name):
847+
# simple table without primary key
848+
sql = "CREATE TABLE \"%s\" (name TEXT NOT NULL)" % table_name
849+
cur.execute(sql)
822850

823-
for i in range(11, 21):
824-
sql = "INSERT INTO test_no_pk (name, geometry) "
825-
sql += "VALUES ('name {id}', GeomFromText('POINT({id} {id})', 4326))".format(id=i)
851+
sql = "SELECT AddGeometryColumn('%s', 'geom', 4326, 'POINT', 'XY')" % table_name
826852
cur.execute(sql)
827853

854+
for i in range(11, 21):
855+
sql = "INSERT INTO \"%s\" (name, geom) " % table_name
856+
sql += "VALUES ('name {id}', GeomFromText('POINT({id} {id})', 4326))".format(id=i)
857+
cur.execute(sql)
858+
859+
_make_table("somedata")
860+
_make_table("some data")
861+
862+
sql = "CREATE VIEW \"some view\" AS SELECT * FROM \"somedata\""
863+
cur.execute(sql)
864+
828865
cur.execute("COMMIT")
829866
con.close()
830867

@@ -840,14 +877,27 @@ def _check_features(vl, offset):
840877
self.assertEqual(f.geometry().asWkt(), 'Point ({id} {id})'.format(id=i))
841878
i += 1
842879

843-
vl_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from test_pk)" (geometry) sql=' % dbname, 'pk', 'spatialite')
880+
vl_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from \\"test pk\\")" (geometry) sql=' % dbname, 'pk', 'spatialite')
844881
self.assertTrue(vl_pk.isValid())
845882
_check_features(vl_pk, 0)
846883

847-
vl_no_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from test_no_pk)" (geometry) sql=' % dbname, 'pk', 'spatialite')
884+
vl_no_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from somedata)" (geom) sql=' % dbname, 'pk', 'spatialite')
885+
self.assertTrue(vl_no_pk.isValid())
886+
_check_features(vl_no_pk, 10)
887+
888+
vl_no_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from \\"some data\\")" (geom) sql=' % dbname, 'pk', 'spatialite')
848889
self.assertTrue(vl_no_pk.isValid())
849890
_check_features(vl_no_pk, 10)
850891

892+
# Test regression when sending queries with aliased tables from DB manager
893+
self._aliased_sql_helper(dbname)
894+
895+
def testAliasedQueries(self):
896+
"""Test regression when sending queries with aliased tables from DB manager"""
897+
898+
dbname = TEST_DATA_DIR + '/provider/spatialite.db'
899+
self._aliased_sql_helper(dbname)
900+
851901

852902
if __name__ == '__main__':
853903
unittest.main()
6 KB
Binary file not shown.

0 commit comments

Comments
 (0)