mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-10-06 03:04:13 +02:00
Merge pull request #985 from ispyisail/fix/query-row-cap
A query stored in a project file can hang QElectroTech for ever
This commit is contained in:
@@ -60,6 +60,26 @@ class projectDataBase : public QObject
|
||||
QETProject *project() const;
|
||||
QSqlQuery newQuery(const QString &query = QString(), QString *error = nullptr);
|
||||
static bool isReadOnlySelect(const QString &query, QString *error = nullptr);
|
||||
|
||||
/**
|
||||
The most rows any caller reads out of one query result.
|
||||
|
||||
A SELECT is not bounded by how much data the project holds:
|
||||
SQLite produces rows lazily, so a query that never stops
|
||||
producing them makes the loop that reads them never stop
|
||||
either. A recursive CTE does exactly that in one line, and
|
||||
a <graphics_table>'s <query> is stored in the .qet and run
|
||||
on load -- so the text can arrive from a file rather than
|
||||
from the person at the keyboard, and opening that file is
|
||||
the whole attack.
|
||||
|
||||
100000 is far above any real result: the largest table in
|
||||
the shipped examples is 396 rows. It is a backstop, not a
|
||||
page size -- a caller that hits it has almost certainly
|
||||
been handed something it should not run to completion, and
|
||||
says so rather than truncating quietly.
|
||||
*/
|
||||
static constexpr int MaxResultRows = 100000;
|
||||
QSqlDatabase database() const {return m_data_base;}
|
||||
int excludedConductorCount() const;
|
||||
|
||||
|
||||
@@ -379,6 +379,21 @@ void ProjectDBModel::fillValue()
|
||||
|
||||
while (query_.next())
|
||||
{
|
||||
//This query text comes out of the project file, so its result is
|
||||
//not bounded by anything the project actually contains: a
|
||||
//recursive CTE produces rows for as long as anyone reads them.
|
||||
//Without this, opening such a file hangs QElectroTech at 100% CPU
|
||||
//while m_record grows until memory runs out. @see
|
||||
//projectDataBase::MaxResultRows.
|
||||
if (m_record.size() >= projectDataBase::MaxResultRows) {
|
||||
qWarning().noquote()
|
||||
<< "ProjectDBModel: query stopped after"
|
||||
<< projectDataBase::MaxResultRows
|
||||
<< "rows, which is far more than a folio table can show."
|
||||
<< "The table is incomplete. Query:" << m_query;
|
||||
break;
|
||||
}
|
||||
|
||||
QStringList record_;
|
||||
auto i=0;
|
||||
while (query_.value(i).isValid())
|
||||
|
||||
@@ -1217,6 +1217,9 @@ QStringList QetScriptApi::tables() const
|
||||
|
||||
@return the rows; empty on refusal or SQL error, with queryError()
|
||||
saying which. An empty result and a failure are not the same thing.
|
||||
A result that ran past projectDataBase::MaxResultRows is cut there and
|
||||
queryError() says so, so a truncated list is never mistaken for a
|
||||
complete one.
|
||||
*/
|
||||
QVariantList QetScriptApi::query(const QString &sql)
|
||||
{
|
||||
@@ -1243,6 +1246,22 @@ QVariantList QetScriptApi::query(const QString &sql)
|
||||
const QSqlRecord record = q.record();
|
||||
while (q.next())
|
||||
{
|
||||
//SQLite produces rows lazily, so a query that never stops
|
||||
//producing them makes this loop never stop either -- "WITH
|
||||
//RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c) SELECT n
|
||||
//FROM c" is one line and runs until memory is gone. The script
|
||||
//engine's own 30 s interrupt does not reach here: that aborts
|
||||
//JavaScript execution, and this is C++ inside a single call.
|
||||
//@see projectDataBase::MaxResultRows.
|
||||
if (rows.size() >= projectDataBase::MaxResultRows) {
|
||||
m_query_error = QStringLiteral(
|
||||
"result truncated at %1 rows; add a LIMIT or a "
|
||||
"WHERE clause")
|
||||
.arg(projectDataBase::MaxResultRows);
|
||||
log(QStringLiteral("qet.query: %1").arg(m_query_error));
|
||||
break;
|
||||
}
|
||||
|
||||
QVariantMap row;
|
||||
for (int i = 0 ; i < record.count() ; ++i) {
|
||||
row.insert(record.fieldName(i), q.value(i));
|
||||
|
||||
Reference in New Issue
Block a user