diff --git a/sources/dataBase/projectdatabase.h b/sources/dataBase/projectdatabase.h index 69ff9f6fc..32f3153ca 100644 --- a/sources/dataBase/projectdatabase.h +++ b/sources/dataBase/projectdatabase.h @@ -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 's 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; diff --git a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp index 1fe814b6a..ca07e5ba8 100644 --- a/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp +++ b/sources/qetgraphicsitem/ViewItem/projectdbmodel.cpp @@ -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()) diff --git a/sources/scripting/qetscriptapi.cpp b/sources/scripting/qetscriptapi.cpp index c8f30b417..895f1a492 100644 --- a/sources/scripting/qetscriptapi.cpp +++ b/sources/scripting/qetscriptapi.cpp @@ -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));