Skip to content

bug(sqlite): column inference for empty result silently re-executes multi-statement writes #1005

Description

@ZhuchkaTriplesix

Summary

SqliteConnection.inferQueryColumns (lib/core/database/sqlite_connection.dart:316) is called by SqliteSqlWorkspace._runQuery (lib/features/sqlite/sqlite_sql_workspace.dart:385) whenever a read-only query returns zero rows, so the grid can still show column headers. It infers columns by running:

await _db!.execute('CREATE TEMP VIEW IF NOT EXISTS $viewName AS $cleanSql');

sqflite's Database.execute (unlike rawQuery) executes every statement in the string it's given, not just the first one. rawQuery silently stops after the first statement, which is why the empty-result branch is reached in the first place — that first statement produced 0 rows. But if the user's input contains more than one statement (e.g. SELECT * FROM t WHERE 0; DELETE FROM t), the first statement is what rawQuery executed and returned (0 rows), and the second statement gets executed as a side effect of building the TEMP VIEW probe, since execute doesn't stop at the first ;.

Repro (confirmed via a throwaway test against sqflite_common_ffi)

final db = await databaseFactoryFfi.openDatabase(inMemoryDatabasePath,
    options: OpenDatabaseOptions(singleInstance: false));
await db.execute('CREATE TABLE t(x)');
await db.execute('INSERT INTO t VALUES (1),(2)');
await db.rawQuery('SELECT 1 WHERE 0; DELETE FROM t');
// table t still has 2 rows here (rawQuery ignored the second statement)
await db.execute('CREATE TEMP VIEW v AS SELECT 1 WHERE 0; DELETE FROM t');
// table t now has 0 rows: the DELETE ran as part of the "harmless" view probe

In the app: open a SQLite table with data, type SELECT * FROM t WHERE 0; DELETE FROM t in the SQL workspace, run it. The grid shows an empty result (expected), but the DELETE silently executes because the result was empty and column inference kicked in.

Why this slips past the destructive-SQL guard

DestructiveSqlDetector scans each statement in the raw SQL for confirmation, but the actual deletion here doesn't happen via the user's own execute path — it happens inside inferQueryColumns's TEMP VIEW probe, which runs unconditionally with no destructive-SQL check at all.

Suggested fix

  • Refuse column inference (return const []) when the statement text contains more than one statement (semicolon-separated, ignoring the trailing one and string/comment content).
  • Or: parse/validate that cleanSql is a single statement before ever passing it to execute.

Scope

Only writable (non-read-only) SQLite connections are affected, since a read-only connection would reject the DELETE outright. Found during a code-quality review of the dev branch (147-file diff since main) rather than reported by a user.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdata-gridInteractive data grid, cell editor, filtering, groupingssqliteSQLite database driver and workspace

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions