Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ target_sources(${PROJECT_NAME}
src/Palette.h
src/CondFormat.h
src/RunSql.h
src/JsonPathChecker.h
src/ProxyDialog.h
src/SelectItemsPopup.h
src/TableBrowser.h
Expand Down
62 changes: 62 additions & 0 deletions src/JsonPathChecker.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
#ifndef JSONPATHCHECKER_H
#define JSONPATHCHECKER_H

#include <QString>
#include <QRegularExpression>
#include <QCoreApplication>

/**
* Detects invalid SQLite JSON path syntax in a SQL statement and returns a
* warning message if found, or an empty string if the statement looks fine.
*
* The specific pattern detected here is a bare negative array index inside a
* JSON path expression, e.g. '$[-1]'. This is valid syntax in MySQL/MariaDB
* but is NOT valid in SQLite. SQLite uses the '#' symbol to refer to the array
* length, so the correct SQLite equivalent for "last element" is '$[#-1]', not
* '$[-1]'.
*
* The danger of this mistake is that SQLite silently returns NULL for the
* invalid path instead of raising an error. When such an expression appears in
* a WHERE clause, it causes rows to be silently dropped from the result set —
* a data-loss bug that is very hard to diagnose.
*
* See: https://github.com/sqlitebrowser/sqlitebrowser/issues/4113
* See: https://sqlite.org/json1.html (path syntax section)
*/
inline QString checkForInvalidJsonPaths(const QString& query)
{
// Match any JSON path step that uses a bare negative array index, e.g.:
// $[-1] — negative index at the root
// $.array[-1] — negative index after a key step
// $[0][-1] — negative index after another array step
//
// The three alternates in the leading group cover these three cases:
// \$ — root anchor (e.g. $[-1])
// \.[\w*@]+ — key step (e.g. .array[-1])
// \] — array step (e.g. [0][-1])
//
// The negative look-ahead (?!#) ensures we do NOT match the valid SQLite
// form $[#-N], which uses '#' as the array-length placeholder.
// Capture group 1 isolates the bare negative index step (e.g. '[-1]') so
// the warning message shows the problematic part without the leading anchor.
static const QRegularExpression invalidJsonPathRx(
QStringLiteral(R"((?:\$|\.[\w*@]+|\])(\[(?!#)-\d+\]))"),
QRegularExpression::CaseInsensitiveOption
);

const QRegularExpressionMatch match = invalidJsonPathRx.match(query);
if(match.hasMatch())
{
return QCoreApplication::translate("RunSql",
"Warning: the JSON path step '%1' uses a bare negative array index which is not "
"supported by SQLite. SQLite silently returns NULL for such paths, which may "
"cause rows to be silently missing from your results. "
"Use '[#-N]' syntax instead (e.g. '$[#-1]' for the last element). "
"See: https://sqlite.org/json1.html")
.arg(match.captured(1));
}

return {};
}

#endif // JSONPATHCHECKER_H
16 changes: 15 additions & 1 deletion src/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1245,7 +1245,12 @@ void MainWindow::executeQuery()
if (char_at_index == '\r' || char_at_index == '\n') {
execute_from_line++;
// The next lines could be empty, so skip all of them too.
while(editor->text(execute_from_line).trimmed().isEmpty())
// Guard against running off the end of the document: if the query ends
// with trailing blank lines (as happens with some SQL samples) the
// previous unbounded loop would spin forever because QScintilla returns
// an empty string for out-of-range line numbers.
while(execute_from_line <= editor->lines() &&
editor->text(execute_from_line).trimmed().isEmpty())
execute_from_line++;
execute_from_index = 0;
}
Expand Down Expand Up @@ -1293,6 +1298,15 @@ void MainWindow::executeQuery()

query_logger(false, status_message, from_position, to_position);
}, Qt::QueuedConnection);
// A statementWarning is emitted when a suspicious pattern is detected that
// SQLite would otherwise silently accept (e.g. a bare negative JSON path
// index like '$[-1]' — see issue #4113). The query still runs; this just
// surfaces the warning in the SQL results area so the user notices.
// We pass ok=true so the editor does NOT highlight the statement as an
// error — the query executed successfully, it is just potentially wrong.
connect(execute_sql_worker.get(), &RunSql::statementWarning, sqlWidget, [query_logger](const QString& warning_message, int from_position, int to_position) {
query_logger(true, warning_message, from_position, to_position);
}, Qt::QueuedConnection);
connect(execute_sql_worker.get(), &RunSql::statementExecuted, sqlWidget, [query_logger, this](const QString& status_message, int from_position, int to_position) {
ui->actionSqlResultsSave->setEnabled(false);
ui->actionSqlResultsSaveAsView->setEnabled(false);
Expand Down
10 changes: 10 additions & 0 deletions src/RunSql.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
#include "sqlite.h"
#include "sqlitedb.h"
#include "Data.h"
#include "JsonPathChecker.h"

#include <chrono>
#include <QApplication>
Expand Down Expand Up @@ -108,6 +109,14 @@ bool RunSql::executeNextStatement()
QString error;
if (sql3status == SQLITE_OK)
{
// Check for known silent-failure patterns before executing. These are
// cases where SQLite will return success without an error but may
// silently drop data (e.g. rows in a WHERE clause) due to an invalid
// argument such as a bare negative index in a JSON path.
const QString json_path_warning = checkForInvalidJsonPaths(executed_query);
if(!json_path_warning.isEmpty())
emit statementWarning(json_path_warning, execute_current_position, end_of_current_statement_position);

// What type of query was this?
StatementType query_type = getQueryType(executed_query);

Expand Down Expand Up @@ -247,6 +256,7 @@ bool RunSql::executeNextStatement()
break;
}
case SQLITE_MISUSE:
error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
break;
default:
error = QString::fromUtf8(sqlite3_errmsg(pDb.get()));
Expand Down
1 change: 1 addition & 0 deletions src/RunSql.h
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ class RunSql : public QThread
void statementErrored(QString message, int from_position, int to_position);
void statementExecuted(QString message, int from_position, int to_position);
void statementReturnsRows(QString query, int from_position, int to_position, qint64 time_in_ms);
void statementWarning(QString message, int from_position, int to_position);
void structureUpdated();

/**
Expand Down
Loading