Skip to content

refactor: enforce read-only queries via SQLite PRAGMA instead of regex - #13

Merged
mpicciolli merged 7 commits into
mainfrom
feat/query-only-read-enforcement
Jul 2, 2026
Merged

refactor: enforce read-only queries via SQLite PRAGMA instead of regex#13
mpicciolli merged 7 commits into
mainfrom
feat/query-only-read-enforcement

Conversation

@mpicciolli

@mpicciolli mpicciolli commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Summary

Replaces the regex keyword blocklist in assertReadOnlyQuery with two complementary layers:

  • Engine-level enforcementwithSaveDb runs PRAGMA query_only = ON on the in-memory database, so SQLite itself rejects any write. This is the reliable backstop.
  • Statement-level validationassertReadOnlyQuery now delegates SQL parsing to sql-query-identifier instead of scanning the raw query text, and requires the input to be exactly one read (LISTING) statement.

Together this removes the false positives the blocklist produced on legitimate read queries, and rejects write attempts up front — including ones a text scan would miss, such as a WITH … DELETE CTE, or a ; hidden inside a string literal.

What changed

  • src/save-db.ts — apply PRAGMA query_only = ON right after loading the save, before the callback runs any query.
  • src/tools/query-save.tsassertReadOnlyQuery uses sql-query-identifier's identify() (non-strict, sqlite dialect) to reject empty input, stacked statements, and anything whose single statement isn't a read (executionType !== "LISTING"). Because it tokenizes SQL properly, a ; inside a string literal, comment or quoted identifier is no longer mistaken for a statement separator, and CTEs are classified by their leaf operation (WITH … SELECT reads, WITH … DELETE writes). explainQueryError still maps the engine's read-only error and missing-table/column errors to clear messages.
  • package.json — add the sql-query-identifier dependency (~100 KB, no runtime deps).
  • Tests updated accordingly.

Behaviour

Case Before (regex) After
WITH x AS (...) DELETE FROM foo passed through ⚠️ rejected up front ✅
SELECT ';' AS x / ... WHERE note = 'a;b' wrongly rejected ❌ accepted ✅
SELECT REPLACE('a','b','c') wrongly rejected ❌ accepted ✅
... LIKE '%create%' wrongly rejected ❌ accepted ✅
ATTACH / PRAGMA / DDL openers blocked blocked ✅
Stacked statements blocked blocked ✅

PRAGMA query_only = ON remains the engine-level backstop, so even a write that somehow reached the database would still fail with attempt to write a readonly database.

mpicciolli and others added 2 commits July 1, 2026 19:09
Replace the keyword blocklist in assertReadOnlyQuery with engine-level
enforcement: withSaveDb now runs `PRAGMA query_only = ON` on the in-memory
database, so SQLite itself rejects any write — including cases a text scan
would miss, such as a `WITH … DELETE` CTE.

The static guard keeps only what the engine can't cover: rejecting stacked
statements (which also shuts out ATTACH/DETACH) and giving a fast, friendly
error for a non-SELECT/WITH opener. This removes the regex false positives,
so legitimate reads like `SELECT REPLACE(...)` or `LIKE '%create%'` now work.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings July 1, 2026 23:12

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors read-only enforcement for save SQL queries by moving from a regex-based keyword blocklist to SQLite engine-level enforcement via PRAGMA query_only = ON, while keeping a small static guard to prevent stacked statements and non-SELECT/WITH openers.

Changes:

  • Enable SQLite read-only mode (PRAGMA query_only = ON) for every withSaveDb session.
  • Simplify assertReadOnlyQuery to focus on single-statement + opener validation, and map engine read-only failures to a clearer user message.
  • Update/extend tests to reflect the new enforcement approach and eliminate prior false positives.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
src/save-db.ts Sets PRAGMA query_only = ON on the in-memory DB before tool callbacks run.
src/tools/query-save.ts Removes regex keyword blocklist; relies on engine enforcement and adds friendlier error mapping.
test/save-db.test.ts Updates DB mock to include run() and adds an order/assertion test for PRAGMA configuration.
test/query-save.test.ts Updates guard tests to accept previously-false-positive read queries and reflect engine enforcement.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/tools/query-save.ts

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.

Comment thread src/tools/query-save.ts Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 7 changed files in this pull request and generated 2 comments.

Comment thread test/tools/query-save.test.ts
Comment thread src/save-db.ts
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@mpicciolli
mpicciolli merged commit c54a207 into main Jul 2, 2026
2 checks passed
@mpicciolli
mpicciolli deleted the feat/query-only-read-enforcement branch July 2, 2026 22:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants