Skip to content

Commit d4439b2

Browse files
committed
Fix spatialite exotic query layers (aliased, nested, joined ...)
Fixes #20674 (again) “It does not matter how slowly you go as long as you do not stop.” ― Confucius
1 parent 3618d63 commit d4439b2

File tree

3 files changed

+141
-42
lines changed

3 files changed

+141
-42
lines changed

src/providers/spatialite/qgsspatialiteprovider.cpp

+112-30
Original file line numberDiff line numberDiff line change
@@ -1123,6 +1123,34 @@ void QgsSpatiaLiteProvider::determineViewPrimaryKey()
11231123
}
11241124
}
11251125

1126+
QList<QString> 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 relations: %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,117 @@ 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+
QString pk { QStringLiteral( "%1.%2" ).arg( quotedIdentifier( alias ) ).arg( pks.first() ) };
4670+
QString newSql( mQuery.replace( injectionRe,
4671+
QStringLiteral( R"re(SELECT %1.%2, \1)re" )
4672+
.arg( quotedIdentifier( tableIdentifier ) )
4673+
.arg( pks.first() ) ) );
4674+
sql = QStringLiteral( "SELECT %1 FROM %2 LIMIT 1" ).arg( pk ).arg( newSql );
46234675
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
46244676
if ( ret == SQLITE_OK && rows == 1 )
46254677
{
46264678
mQuery = newSql;
4627-
mPrimaryKey = QStringLiteral( "ROWID" );
4628-
mRowidInjectedInQuery = true;
4679+
mPrimaryKey = pks.first( );
46294680
}
46304681
}
4631-
// 4. if it does not work, simply clear the message and fallback to the original behavior
4632-
if ( errMsg )
4682+
4683+
// If there is still no primary key, check if we can get use the ROWID from the table that provides the geometry
4684+
if ( mPrimaryKey.isEmpty() )
46334685
{
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;
4686+
// 4. check if the table has a usable ROWID
4687+
if ( ! queryGeomTableName.isEmpty() )
4688+
{
4689+
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( quotedIdentifier( queryGeomTableName ) );
4690+
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4691+
if ( ret != SQLITE_OK || rows != 1 )
4692+
{
4693+
queryGeomTableName = QString();
4694+
}
4695+
}
4696+
// 5. check if ROWID injection works
4697+
if ( ! queryGeomTableName.isEmpty() )
4698+
{
4699+
const QString newSql( mQuery.replace( injectionRe,
4700+
QStringLiteral( R"re(SELECT %1.%2, \1)re" )
4701+
.arg( quotedIdentifier( tableIdentifier ),
4702+
QStringLiteral( "ROWID" ) ) ) );
4703+
sql = QStringLiteral( "SELECT ROWID FROM %1 WHERE ROWID IS NOT NULL LIMIT 1" ).arg( newSql );
4704+
ret = sqlite3_get_table( mSqliteHandle, sql.toUtf8().constData(), &results, &rows, &columns, &errMsg );
4705+
if ( ret == SQLITE_OK && rows == 1 )
4706+
{
4707+
mQuery = newSql;
4708+
mPrimaryKey = QStringLiteral( "ROWID" );
4709+
mRowidInjectedInQuery = true;
4710+
}
4711+
}
4712+
// 6. if it does not work, simply clear the message and fallback to the original behavior
4713+
if ( errMsg )
4714+
{
4715+
QgsMessageLog::logMessage( tr( "SQLite error while trying to inject ROWID: %2\nSQL: %1" ).arg( sql, errMsg ), tr( "SpatiaLite" ) );
4716+
sqlite3_free( errMsg );
4717+
errMsg = nullptr;
4718+
}
46374719
}
46384720
sqlite3_finalize( stmt );
46394721
mIsQuery = true;

src/providers/spatialite/qgsspatialiteprovider.h

+3
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+
QList<QString> 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

+26-12
Original file line numberDiff line numberDiff line change
@@ -789,6 +789,23 @@ def testLoadStyle(self):
789789
err, ok = vl.loadDefaultStyle()
790790
self.assertTrue(ok)
791791

792+
def _aliased_sql_helper(self, dbname):
793+
queries = (
794+
'(select sd.* from somedata as sd left join somedata as sd2 on ( sd2.name = sd.name ))',
795+
'(select sd.* from \\"somedata\\" as sd left join \\"somedata\\" as sd2 on ( sd2.name = sd.name ))',
796+
"(SELECT * FROM somedata as my_alias1\n)",
797+
"(SELECT * FROM somedata as my_alias2)",
798+
"(SELECT * FROM somedata AS my_alias3)",
799+
'(SELECT * FROM \\"somedata\\" as my_alias4\n)',
800+
'(SELECT * FROM (SELECT * FROM \\"somedata\\"))',
801+
'(SELECT my_alias5.* FROM (SELECT * FROM \\"somedata\\") AS my_alias5)',
802+
'(SELECT my_alias5.* FROM (SELECT * FROM \\"somedata\\" as my_alias\n) AS my_alias5)',
803+
'(SELECT my_alias6.* FROM (SELECT * FROM \\"somedata\\" as my_alias\n) AS my_alias6\n)',
804+
)
805+
for sql in queries:
806+
vl = QgsVectorLayer('dbname=\'{}\' table="{}" (geom) sql='.format(dbname, sql), 'test', 'spatialite')
807+
self.assertTrue(vl.isValid(), 'dbname: {} - sql: {}'.format(dbname, sql))
808+
792809
def testPkLessQuery(self):
793810
"""Test if features in queries with/without pk can be retrieved by id"""
794811
# create test db
@@ -814,14 +831,14 @@ def testPkLessQuery(self):
814831
cur.execute(sql)
815832

816833
# simple table without primary key
817-
sql = "CREATE TABLE test_no_pk (name TEXT NOT NULL)"
834+
sql = "CREATE TABLE somedata (name TEXT NOT NULL)"
818835
cur.execute(sql)
819836

820-
sql = "SELECT AddGeometryColumn('test_no_pk', 'geometry', 4326, 'POINT', 'XY')"
837+
sql = "SELECT AddGeometryColumn('somedata', 'geom', 4326, 'POINT', 'XY')"
821838
cur.execute(sql)
822839

823840
for i in range(11, 21):
824-
sql = "INSERT INTO test_no_pk (name, geometry) "
841+
sql = "INSERT INTO somedata (name, geom) "
825842
sql += "VALUES ('name {id}', GeomFromText('POINT({id} {id})', 4326))".format(id=i)
826843
cur.execute(sql)
827844

@@ -844,21 +861,18 @@ def _check_features(vl, offset):
844861
self.assertTrue(vl_pk.isValid())
845862
_check_features(vl_pk, 0)
846863

847-
vl_no_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from test_no_pk)" (geometry) sql=' % dbname, 'pk', 'spatialite')
864+
vl_no_pk = QgsVectorLayer('dbname=\'%s\' table="(select * from somedata)" (geom) sql=' % dbname, 'pk', 'spatialite')
848865
self.assertTrue(vl_no_pk.isValid())
849866
_check_features(vl_no_pk, 10)
850867

868+
# Test regression when sending queries with aliased tables from DB manager
869+
self._aliased_sql_helper(dbname)
870+
851871
def testAliasedQueries(self):
852872
"""Test regression when sending queries with aliased tables from DB manager"""
853873

854-
def _test(sql):
855-
vl = QgsVectorLayer('dbname=\'{}/provider/spatialite.db\' table="{}" (geom) sql='.format(TEST_DATA_DIR, sql), 'test', 'spatialite')
856-
self.assertTrue(vl.isValid())
857-
858-
_test("(SELECT * FROM somedata as my_alias\n)")
859-
_test("(SELECT * FROM somedata as my_alias)")
860-
_test("(SELECT * FROM somedata AS my_alias)")
861-
_test('(SELECT * FROM \\"somedata\\" as my_alias\n)')
874+
dbname = TEST_DATA_DIR + '/provider/spatialite.db'
875+
self._aliased_sql_helper(dbname)
862876

863877

864878
if __name__ == '__main__':

0 commit comments

Comments
 (0)