@@ -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
11271155bool 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\n SQL: %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\n SQL: %1" ).arg ( sql, errMsg ), tr ( " SpatiaLite" ) );
4726+ sqlite3_free ( errMsg );
4727+ errMsg = nullptr ;
4728+ }
46374729 }
46384730 sqlite3_finalize ( stmt );
46394731 mIsQuery = true ;
0 commit comments