mirror of
https://github.com/qelectrotech/qelectrotech-source-mirror.git
synced 2026-09-27 20:44:13 +02:00
Stop a query in a project file from hanging QElectroTech for ever
A <graphics_table>'s <query> is stored in the .qet and executed when the project loads. SQLite produces rows lazily, so the cost of that query is not bounded by anything the project contains -- it is bounded by how long the loop reading the rows is willing to run. A recursive CTE takes one line to make that forever: WITH RECURSIVE c(n) AS (SELECT 1 UNION ALL SELECT n+1 FROM c) SELECT n ... Put that in the <query> of any project's summary table and opening the file pins a core at 100% and grows ProjectDBModel::m_record until memory runs out. Measured on examples/industrial.qet with the query swapped, built from master: clean --export-bom 3.6 s, 396 rows, exit 0 poisoned --export-bom killed at 90 s, still going, no output No scripting, no MCP, no flag beyond an ordinary export. Opening the file in the editor is the same code path. QetScriptApi::query() has the identical loop, and the script engine's own 30 s interrupt does not reach it: that aborts JavaScript, and this is C++ inside a single call. Left alone it hung a --run for 45 s until the harness killed it. Both loops now stop at projectDataBase::MaxResultRows (100000) and say so. That is a backstop, not a page size: the largest table in the shipped examples is 396 rows, and a caller that reaches 100000 has been handed something it should not run to completion. It is not silent either way -- the model logs the offending query text, and qet.query() sets queryError(), so a truncated result is never mistaken for a complete one. clean --export-bom 3.6 s, 396 rows, exit 0 (unchanged) poisoned --export-bom 20.2 s, 396 rows, exit 0, warning names the query qet.query(recursive CTE) 3.8 s, 100000 rows, queryError() set Reverting each cap restores the hang, so both checks discriminate. Related to #983, which fixes a different flaw reachable through the same stored query. Neither depends on the other. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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