#2087

ENG-803: EXPLAIN ANALYSE, a quoted analyze option and SELECT INTO are refused by a read-only rule

Openrogefmwants to merge intofrom

IMPORTANT

Every PR MUST have exactly one of these labels, the merge is blocked otherwise:

  • major / minor / patch publishes a new release on merge with the corresponding semver bump.
  • skip-release merges without publishing a release (use for docs, CI changes, refactors, etc.).

📝 Description

A read-only operation rule let Postgres writes through. EXPLAIN ANALYSE (British spelling) and a quoted "analyze" option ran their DELETE but were classified as a plan. SELECT ... INTO t created t but was classified as select, or as unknown in parentheses.

The sidecar lexer now reads ANALYSE and a quoted option name as ANALYZE, gives Postgres SELECT ... INTO a create effect, and reads a parenthesis where a statement may begin as a query.

📣 User-facing impact

A sidecar rule that refuses writes now also refuses EXPLAIN ANALYSE, EXPLAIN ("analyze"), ANALYSE and SELECT ... INTO on Postgres.

🔗 Related Issue

ENG-803

🚀 Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📚 Documentation update
  • 🎨 Style/UI update
  • ♻️ Code refactor
  • ⚡ Performance improvement
  • ✅ Test update
  • 🔧 Build configuration change
  • 🧹 Chore

📋 Changes Made

  • sidecar/lexer/keywords.go: analyse is an EXPLAIN option and a statement verb, the same as analyze.
  • sidecar/lexer/analyze.go: a quoted name in EXPLAIN's option list counts as ANALYZE unless it names another known option. PostgreSQL compares a quoted option name verbatim.
  • sidecar/lexer/analyze.go: an ANALYZE option whose whole value is false, off or 0 does not count, so EXPLAIN (ANALYZE false) DELETE is a plan, as in PostgreSQL. 0x1 or 0_1 is 1 in PostgreSQL 16 and stays on. One ANALYZE that is on still wins.
  • sidecar/lexer/analyze.go: on Postgres, SELECT ... INTO t adds a create effect. The relation was already reported as written.
  • sidecar/lexer/analyze.go: a ( where a statement may begin, before SELECT, WITH, VALUES, TABLE or another (, opens a query, so the SELECT inside it is a statement head: (SELECT ...) is a select, not unknown. Any other ( stays an expression: CASE ... ELSE (update) END reads a column.
  • Tests: TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto in sidecar/inspect/bypass_test.go, TestParenthesizedExpressionIsNotAStatement, new cases in TestExplainPlansButAnalyzeExecutes and TestObjectCreatingForms, and the statements in the lexer/conformance corpus.

🧪 Testing

Result: on main, a sidecar with the read-only rule let six Postgres statements delete rows and create tables, each audited as an allowed select or unknown. With the fix, the sidecar refuses all six as delete or create, and the database does not change. Full write-up: EVIDENCE.md.

1. Does a read-only lane refuse these writes? The real hoop-inspect built from each side, in front of Postgres 16, with the Marketplace preset's read-only rule and no license. The last table is read without the sidecar.

2. Do plans and reads still pass? Yes, CASE reads of columns named update and delete included. (SELECT ...) moves from unknown to select.

3. Does the value of ANALYZE decide, as in Postgres? false and 0 only plan: main refused them, the branch allows them. 0x1 and 0o1 are 1 in Postgres 16 and run the DELETE: main let "analyze" 0o1 delete a row, the branch refuses both.

What it solves: a read-only rule on a Postgres sidecar lane, as the Marketplace preset and the agent templates ship it, refuses every write ENG-803 found, and lets the plans Postgres only plans.

Definition of done (ENG-803)

  • EXPLAIN ANALYSE DELETE, EXPLAIN ("analyze") DELETE and EXPLAIN (FORMAT JSON, "analyze") DELETE classify as delete, and a read-only operation rule refuses them: item 1.
  • SELECT ... INTO t and SELECT ... INTO TEMP t classify as create, in parentheses too: item 1.
  • Each case is in sidecar/inspect/bypass_test.go as an operator's verdict, and fails on main: TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto (9 refusals fail on main).
  • lexer/conformance passes with the cases in its corpus (101/103 exact, 2 conceded, 0 wrong); EXPLAIN ANALYSE and (SELECT * INTO ...) fail there on main.

Guards: TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto, TestExplainPlansButAnalyzeExecutes, TestObjectCreatingForms, TestParenthesizedExpressionIsNotAStatement, and the lexer/conformance corpus.

Re-run

  • cd sidecar && go test ./lexer/ ./inspect/ -run 'ReadOnlyRule|ExplainPlans|ObjectCreating|ParenthesizedExpression' -v
  • cd sidecar/lexer/conformance && CGO_ENABLED=0 go test ./...

Test Configuration:

  • Browser(s): n/a
  • OS: macOS; hoop-inspect built from origin/main 9d4534086 and the branch 5b8842a7c, Postgres 16 in Docker (QA: Postgres 17).

Tests performed:

  • Unit tests pass
  • Integration tests pass (CI)
  • Manual testing completed: independent QA passed in round 5 on 5b8842a7c, on real relays in front of Postgres 17, simple and extended protocol

📸 Screenshots (if applicable)

In Testing above.

✅ Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes
  • I have checked my code and corrected any misspellings

📄 Additional Notes

  • Scope is the Postgres dialect. T-SQL's SELECT INTO also creates a table and is still a select; ENG-905 covers dialects outside Postgres.
  • The parenthesis change is not Postgres only: in every dialect, (SELECT ...) and (SELECT ...) UNION (SELECT ...) are now select, not unknown. A rule that names unknown no longer refuses them.
  • Checked with a differential: main and the branch on the lexer's test literals plus 40,000 seeded inputs, in every dialect. PostgreSQL's parser judged each changed Postgres input: no new write on a statement Postgres accepts, and no write lost.
  • EXPLAIN (ANALYZE false) DELETE was refused on main, although Postgres only plans it. It passes now, quoted or not (Qodo's review).
  • After the release: the Marketplace module, the agent templates and their READMEs name ENG-803 as a known gap. Remove those notes when their sidecar pin moves past this release.

🤖 Generated with Claude Code

Activity

21
sandromello2d ago

✅ Build Completed with Success, Version=2087.0.0-g16986a9

qodo-code-review[bot]2d ago

PR Summary by Qodo

Close PostgreSQL read-only rule bypasses in SQL classification

🐞 Bug fix 🧪 Tests 🕐 40+ Minutes

AI Description

• Classify EXPLAIN ANALYSE and quoted analyze options as executing their wrapped statements.
• Report PostgreSQL SELECT INTO, including parenthesized forms, as table creation.
• Test read-only refusals while preserving ordinary plans, selects, and parenthesized expressions.
Diagram

graph TD
  SQL["PostgreSQL SQL"] --> Tokens["Lexer tokens"] --> Context["Statement context"] --> Effects["Operation effects"] --> Inspect["Inspect mapping"] --> Rule["Read-only rule"]
High-Level Assessment

The following are alternative approaches to this PR:

1. Use a PostgreSQL syntax parser
  • ➕ Could model EXPLAIN options and SELECT INTO from parsed syntax rather than lexer context.
  • ➖ Adds a substantial dependency and does not replace the existing multi-dialect classifier or policy mapping.

Recommendation: Keep the targeted lexer changes: they fit the existing effect-based policy pipeline and cover the demonstrated bypasses. A full parser is disproportionate to this fix, though continued dialect edge cases may warrant revisiting that trade-off.

Files changed (5) +223 / -5

Bug fix (2) +72 / -5
analyze.goRecognize executing EXPLAIN options and SELECT INTO effects +68/-3

Recognize executing EXPLAIN options and SELECT INTO effects

• Treats quoted EXPLAIN option names conservatively, recognizes ANALYSE, and adds a create effect for PostgreSQL SELECT INTO. Distinguishes parenthesized queries from expressions so nested SELECT statements retain their head position.

sidecar/lexer/analyze.go

keywords.goRecognize ANALYSE as a verb and EXPLAIN option +4/-2

Recognize ANALYSE as a verb and EXPLAIN option

• Adds PostgreSQL’s British spelling to the statement-verb and EXPLAIN modifier tables.

sidecar/lexer/keywords.go

Tests (3) +151 / -0
bypass_test.goVerify read-only rules refuse formerly allowed writes +72/-0

Verify read-only rules refuse formerly allowed writes

• Adds policy-level cases for executing EXPLAIN forms, ANALYSE, and SELECT INTO, including nested parentheses. Confirms ordinary plans, selects, and parenthesized column expressions remain allowed.

sidecar/inspect/bypass_test.go

oracle_test.goExpand SQL conformance corpus with bypass forms +9/-0

Expand SQL conformance corpus with bypass forms

• Adds executing EXPLAIN ANALYSE and quoted-option statements, plus temporary and parenthesized SELECT INTO examples.

sidecar/lexer/conformance/oracle_test.go

lexer_test.goCover write effects and parenthesis classification +70/-0

Cover write effects and parenthesis classification

• Tests executing versus non-executing EXPLAIN options and PostgreSQL SELECT INTO variants. Adds cross-dialect checks that parenthesized expressions containing statement-like column names are not classified as writes.

sidecar/lexer/lexer_test.go

qodo-code-review[bot]2d ago

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (2) 📎 Requirement gaps (1) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


1. Zero-valued options wrongly execute deletes ✗ Dismissed
Description
explainOptionOff recognizes only a single Number token whose text is 0, so split zero-valued
literals such as 0x0, 0o0, 0b0 and 0_0 are treated as enabled. PostgreSQL accepts these
literals as zero option values, causing the following DELETE to be reported as executing and
refused by a read-only rule.
Code

sidecar/lexer/analyze.go[619]

+	if !a.explainOptionName(i) || !a.puncts(i+2, ",") && !a.puncts(i+2, ")") {
Relevance

●●● Strong

Concrete PostgreSQL misclassification; deterministic lexer edge cases receive regression coverage.

PR-#2012

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed boundary check requires the value to be one token, while the scanner splits literals
such as 0x0 and 0_0 into Number("0") followed by a word. The analyzer then treats the option
as enabled whenever this check returns false, and the focused tests cover only corresponding nonzero
values.

EXPLAIN write variants are refused on read-only lanes sidecar/lexer/analyze.go[610-623] sidecar/lexer/scan.go[795-806] sidecar/lexer/lexer_test.go[327-355]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Zero-valued non-decimal and underscored PostgreSQL literals are tokenized into multiple tokens, so `EXPLAIN (ANALYZE 0x0)`, `0o0`, `0b0`, and `0_0` are incorrectly treated as executing the wrapped statement.

## Fix Focus Areas
- sidecar/lexer/analyze.go[610-624]
- sidecar/lexer/lexer_test.go[327-355]

## Recommended Fix
Update the EXPLAIN option value handling to recognize PostgreSQL zero-valued non-decimal and underscored literals as `ANALYZE` off while preserving nonzero values as on. Add regression tests for each zero-valued literal form and verify that the wrapped `DELETE` has no write effect.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



2. Plan-only statements are refused as writes ✓ Resolved
Description
The quoted-option branch sets analyzeSeen for "analyze" without checking whether its value
disables execution. When a user runs EXPLAIN ("analyze" false) DELETE FROM customers, the analyzer
retains the delete effect even though the statement only produces a plan, so a read-only rule
refuses it.
Code

sidecar/lexer/analyze.go[R438-439]

+		if t.Kind == Quoted && a.explainOptionName(i) && (t.Text == "analyze" || !wrapperModifier[t.Text]) {
+			a.analyzeSeen = true
Relevance

●●● Strong

The PostgreSQL oracle explicitly covers false/off/0, exposing a deterministic
execution-classification gap.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new branch sets analyzeSeen based on the quoted name alone. result() retains mutating
effects whenever analyzeSeen is true, while the repository's PostgreSQL oracle explicitly treats
an ANALYZE option with false, off, or 0 as non-executing.

sidecar/lexer/analyze.go[438-440] sidecar/lexer/analyze.go[1385-1402] sidecar/lexer/conformance/oracle_test.go[318-341]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A quoted `"analyze"` option is treated as enabled even when its explicit value is `false`, `off`, or `0`, causing plan-only statements to be classified as writes.
## Fix Focus Areas
- sidecar/lexer/analyze.go[438-440]
- sidecar/lexer/lexer_test.go[313-335]
## Recommended Fix
Inspect the quoted option's value before setting `analyzeSeen`, preserving conservative handling of unknown quoted option names. Add regression cases for quoted `"analyze"` with disabling values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. New query forms lack oracle coverage ✓ Resolved
Description
opensQuery now classifies nested parentheses and parenthesized WITH, VALUES, and TABLE forms
as query starts, while quoted EXPLAIN handling also covers undecoded escaped names. The PostgreSQL
conformance corpus includes only a single parenthesized SELECT and plain quoted analyze cases,
so these new branches are not all checked against the PostgreSQL parser.
Code

sidecar/lexer/analyze.go[R757-765]

+func (a *analyzer) opensQuery(i int) bool {
+	if a.puncts(i+1, "(") {
+		return true
+	}
+	if i+1 >= len(a.toks) {
+		return false
+	}
+	next := a.toks[i+1]
+	return next.isWord("select") || next.isWord("with") || next.isWord("values") || next.isWord("table")
Relevance

●● Moderate

Recommendation generated with fallback mode.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation adds classifications for nested parentheses, four query-start keywords, and
escaped quoted EXPLAIN options, but the corpus only contains one parenthesized SELECT form and plain
quoted analyze options.

Rule 3062508: Update lexer conformance corpus for new classification cases sidecar/lexer/analyze.go[438-440] sidecar/lexer/analyze.go[741-765] sidecar/lexer/conformance/oracle_test.go[604-610] sidecar/lexer/conformance/oracle_test.go[653-657]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several lexer classification branches added in this PR are covered by unit tests but not by the PostgreSQL conformance corpus required for oracle validation.

## Fix Focus Areas
- sidecar/lexer/analyze.go[438-440]
- sidecar/lexer/analyze.go[741-742]
- sidecar/lexer/analyze.go[757-765]
- sidecar/lexer/conformance/oracle_test.go[604-610]
- sidecar/lexer/conformance/oracle_test.go[653-657]

## Recommended Fix
Add PostgreSQL corpus cases for nested parenthesized queries, parenthesized `WITH`, `VALUES`, and `TABLE` statements, and the escaped quoted option form such as `EXPLAIN (U&"\\0061nalyze") DELETE ...`. Run the conformance suite and retain the cases in `oracle_test.go`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



4. New option values lack PostgreSQL cases 📘 Rule violation ≡ Correctness ⭐ New
Description
explainOptionOff adds special handling for whole values such as false, off, 0, and
PostgreSQL 16 numeric forms, but the conformance corpus contains no corresponding value cases. The
behavior is covered only by unit tests, so the PostgreSQL comparison path is not exercised for these
newly classified forms.
Code

sidecar/lexer/analyze.go[R610-617]

+// explainOptionOff reports whether the value of the EXPLAIN option named at i
+// turns it off: `EXPLAIN (ANALYZE false) DELETE` plans the DELETE and runs
+// nothing. PostgreSQL reads false, off and 0 as off. The value must be the
+// whole value, ended by a comma or the closing parenthesis: the scanner reads
+// `0x1`, `0_1` or `0b1` as 0 and a word, and PostgreSQL 16 reads each as 1.
+// Any other value leaves the option on, a string literal included, whose text
+// the scanner does not keep: that reading refuses a write. One ANALYZE that is
+// on is enough, so a second one that is off cannot hide it.
Relevance

● Weak

Near-identical corpus-coverage request was explicitly dismissed as a false positive.

PR-#2012

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed analyzer introduces value-sensitive EXPLAIN classification, while the conformance corpus
additions cover bare ANALYSE, quoted analyze, and query forms but no disabled or numeric option
values. The lexer unit tests cover those values separately, which does not satisfy the
corpus-specific coverage requirement.

Rule 3062508: Update lexer conformance corpus for new classification cases sidecar/lexer/analyze.go[610-617] sidecar/lexer/conformance/oracle_test.go[604-613] sidecar/lexer/lexer_test.go[325-355]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `EXPLAIN` option-value handling distinguishes disabled values from values that execute the wrapped statement, but the PostgreSQL conformance corpus does not exercise these new forms.

## Fix Focus Areas
- sidecar/lexer/conformance/oracle_test.go[604-613]
- sidecar/lexer/analyze.go[610-617]

## Recommended Fix
Add conformance corpus entries covering representative `ANALYZE` and quoted `"analyze"` values, including `false`, `off`, `0`, and a PostgreSQL 16 numeric form such as `0x1` or `0_1`. Ensure the corpus runner compares each case with PostgreSQL behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


5. Audit evidence omits affected verdicts 📎 Requirement gap ◔ Observability
Description
The PR's TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto adds in-process assertions but
does not provide before-and-after audit rows for the affected statements. The checklist requires an
audit row for each EXPLAIN and SELECT INTO case, so reviewers cannot verify the historical allowed
verdict and corrected refused verdict from the submitted evidence.
Code

sidecar/inspect/bypass_test.go[R367-370]

+		{"EXPLAIN ANALYSE, the British spelling, is refused as the delete it runs",
+			`EXPLAIN ANALYSE DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"EXPLAIN (ANALYSE) is refused as the delete it runs",
+			`EXPLAIN (ANALYSE) DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
Relevance

● Weak

A closely matching before-and-after verdict-evidence request was rejected as unnecessary
documentation.

PR-#2012

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added tests verify classifications and rule decisions in process, but the PR changes contain no
before-and-after audit rows for the affected statements.

Provide before-and-after audit evidence sidecar/inspect/bypass_test.go[367-370]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR does not include before-and-after audit rows demonstrating the old allowed verdict and the corrected refused verdict for every affected EXPLAIN and SELECT INTO statement.

## Fix Focus Areas
- sidecar/inspect/bypass_test.go[367-384]

## Recommended Fix
Add evidence documenting one before-and-after audit row for each listed EXPLAIN ANALYSE, quoted analyze option, and SELECT INTO case, including the classified operation and read-only rule verdict.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


6. Evasion cases lack verdict-string tests 📘 Rule violation ▣ Testability
Description
TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto supplies SQL statements such as `EXPLAIN
ANALYSE and SELECT INTO to inspectOne` instead of testing the exact operator-facing verdict
strings required for new evasion cases. These additions exercise parser inputs but do not cover the
verdict-string path in inspect/bypass_test.go.
Code

sidecar/inspect/bypass_test.go[R367-372]

+		{"EXPLAIN ANALYSE, the British spelling, is refused as the delete it runs",
+			`EXPLAIN ANALYSE DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"EXPLAIN (ANALYSE) is refused as the delete it runs",
+			`EXPLAIN (ANALYSE) DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"a quoted analyze option is refused as the delete it runs",
+			`EXPLAIN ("analyze") DELETE FROM customers WHERE id = 3`, true, inspect.OpDelete},
Relevance

● Weak

Same verdict-string test request was rejected in a closely matching inspect test precedent.

PR-#1806

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed test adds SQL statements and operation assertions rather than operator-facing
verdict-string inputs, which fails the required coverage shape for new classification evasion
scenarios.

Rule 3062500: Document new classification evasion cases as verdict strings in inspect/bypass_test.go sidecar/inspect/bypass_test.go[356-384]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new read-only evasion scenarios are tested as SQL inputs, but the compliance requirement expects each classification evasion case to also be represented by the exact human-readable verdict string an operator receives.

## Fix Focus Areas
- sidecar/inspect/bypass_test.go[356-417]

## Recommended Fix
Add test cases in `sidecar/inspect/bypass_test.go` using the exact operator-facing verdict strings for the new EXPLAIN, ANALYSE, quoted analyze-option, and SELECT INTO classifications, and assert the expected refusal behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Context sources
✅ Compliance rules (platform): 106 rules
✅ Skills: build-ticket
✅ Web pages:
  +8 more
✅ Cross-repo context — repo relationships
  Explored: repo: hoophq/documentation (sha: e6ab4f1a) — View relationship
  Explored: repo: hoophq/internal-runtime-control (sha: f9e2bc6f) — View relationship
Review mode: Auto: ⚖️ Balanced: Localized lexer fix with read-only bypass implications warrants standard review.
Tip of the day
💡 Did you know, you can add REVIEW.md to your repo root and Qodo follows it on every PR

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Previous reviews

Review updated until commit 5b8842a

Results up to commit 16986a9 ⚖️ Balanced


🐞 Bugs (0) 📘 Rule violations (1) 📎 Requirement gaps (1) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)


1. Plan-only statements are refused as writes ✓ Resolved
Description
The quoted-option branch sets analyzeSeen for "analyze" without checking whether its value
disables execution. When a user runs EXPLAIN ("analyze" false) DELETE FROM customers, the analyzer
retains the delete effect even though the statement only produces a plan, so a read-only rule
refuses it.
Code

sidecar/lexer/analyze.go[R438-439]

+		if t.Kind == Quoted && a.explainOptionName(i) && (t.Text == "analyze" || !wrapperModifier[t.Text]) {
+			a.analyzeSeen = true
Relevance

●●● Strong

The PostgreSQL oracle explicitly covers false/off/0, exposing a deterministic
execution-classification gap.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The new branch sets analyzeSeen based on the quoted name alone. result() retains mutating
effects whenever analyzeSeen is true, while the repository's PostgreSQL oracle explicitly treats
an ANALYZE option with false, off, or 0 as non-executing.

sidecar/lexer/analyze.go[438-440] sidecar/lexer/analyze.go[1385-1402] sidecar/lexer/conformance/oracle_test.go[318-341]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A quoted `"analyze"` option is treated as enabled even when its explicit value is `false`, `off`, or `0`, causing plan-only statements to be classified as writes.
## Fix Focus Areas
- sidecar/lexer/analyze.go[438-440]
- sidecar/lexer/lexer_test.go[313-335]
## Recommended Fix
Inspect the quoted option's value before setting `analyzeSeen`, preserving conservative handling of unknown quoted option names. Add regression cases for quoted `"analyze"` with disabling values.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. New query forms lack oracle coverage ✓ Resolved
Description
opensQuery now classifies nested parentheses and parenthesized WITH, VALUES, and TABLE forms
as query starts, while quoted EXPLAIN handling also covers undecoded escaped names. The PostgreSQL
conformance corpus includes only a single parenthesized SELECT and plain quoted analyze cases,
so these new branches are not all checked against the PostgreSQL parser.
Code

sidecar/lexer/analyze.go[R757-765]

+func (a *analyzer) opensQuery(i int) bool {
+	if a.puncts(i+1, "(") {
+		return true
+	}
+	if i+1 >= len(a.toks) {
+		return false
+	}
+	next := a.toks[i+1]
+	return next.isWord("select") || next.isWord("with") || next.isWord("values") || next.isWord("table")
Relevance

●● Moderate

Recommendation generated with fallback mode.

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The implementation adds classifications for nested parentheses, four query-start keywords, and
escaped quoted EXPLAIN options, but the corpus only contains one parenthesized SELECT form and plain
quoted analyze options.

Rule 3062508: Update lexer conformance corpus for new classification cases sidecar/lexer/analyze.go[438-440] sidecar/lexer/analyze.go[741-765] sidecar/lexer/conformance/oracle_test.go[604-610] sidecar/lexer/conformance/oracle_test.go[653-657]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Several lexer classification branches added in this PR are covered by unit tests but not by the PostgreSQL conformance corpus required for oracle validation.

## Fix Focus Areas
- sidecar/lexer/analyze.go[438-440]
- sidecar/lexer/analyze.go[741-742]
- sidecar/lexer/analyze.go[757-765]
- sidecar/lexer/conformance/oracle_test.go[604-610]
- sidecar/lexer/conformance/oracle_test.go[653-657]

## Recommended Fix
Add PostgreSQL corpus cases for nested parenthesized queries, parenthesized `WITH`, `VALUES`, and `TABLE` statements, and the escaped quoted option form such as `EXPLAIN (U&"\\0061nalyze") DELETE ...`. Run the conformance suite and retain the cases in `oracle_test.go`.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



3. Evasion cases lack verdict-string tests 📘 Rule violation ▣ Testability
Description
TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto supplies SQL statements such as `EXPLAIN
ANALYSE and SELECT INTO to inspectOne` instead of testing the exact operator-facing verdict
strings required for new evasion cases. These additions exercise parser inputs but do not cover the
verdict-string path in inspect/bypass_test.go.
Code

sidecar/inspect/bypass_test.go[R367-372]

+		{"EXPLAIN ANALYSE, the British spelling, is refused as the delete it runs",
+			`EXPLAIN ANALYSE DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"EXPLAIN (ANALYSE) is refused as the delete it runs",
+			`EXPLAIN (ANALYSE) DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"a quoted analyze option is refused as the delete it runs",
+			`EXPLAIN ("analyze") DELETE FROM customers WHERE id = 3`, true, inspect.OpDelete},
Relevance

● Weak

Same verdict-string test request was rejected in a closely matching inspect test precedent.

PR-#1806

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The changed test adds SQL statements and operation assertions rather than operator-facing
verdict-string inputs, which fails the required coverage shape for new classification evasion
scenarios.

Rule 3062500: Document new classification evasion cases as verdict strings in inspect/bypass_test.go sidecar/inspect/bypass_test.go[356-384]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new read-only evasion scenarios are tested as SQL inputs, but the compliance requirement expects each classification evasion case to also be represented by the exact human-readable verdict string an operator receives.

## Fix Focus Areas
- sidecar/inspect/bypass_test.go[356-417]

## Recommended Fix
Add test cases in `sidecar/inspect/bypass_test.go` using the exact operator-facing verdict strings for the new EXPLAIN, ANALYSE, quoted analyze-option, and SELECT INTO classifications, and assert the expected refusal behavior.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


4. Audit evidence omits affected verdicts 📎 Requirement gap ◔ Observability
Description
The PR's TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto adds in-process assertions but
does not provide before-and-after audit rows for the affected statements. The checklist requires an
audit row for each EXPLAIN and SELECT INTO case, so reviewers cannot verify the historical allowed
verdict and corrected refused verdict from the submitted evidence.
Code

sidecar/inspect/bypass_test.go[R367-370]

+		{"EXPLAIN ANALYSE, the British spelling, is refused as the delete it runs",
+			`EXPLAIN ANALYSE DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
+		{"EXPLAIN (ANALYSE) is refused as the delete it runs",
+			`EXPLAIN (ANALYSE) DELETE FROM customers WHERE id = 2`, true, inspect.OpDelete},
Relevance

● Weak

A closely matching before-and-after verdict-evidence request was rejected as unnecessary
documentation.

PR-#2012

ⓘ Recommendations generated based on similar findings in past PRs

Evidence
The added tests verify classifications and rule decisions in process, but the PR changes contain no
before-and-after audit rows for the affected statements.

Provide before-and-after audit evidence sidecar/inspect/bypass_test.go[367-370]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The PR does not include before-and-after audit rows demonstrating the old allowed verdict and the corrected refused verdict for every affected EXPLAIN and SELECT INTO statement.

## Fix Focus Areas
- sidecar/inspect/bypass_test.go[367-384]

## Recommended Fix
Add evidence documenting one before-and-after audit row for each listed EXPLAIN ANALYSE, quoted analyze option, and SELECT INTO case, including the classified operation and read-only rule verdict.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


qodo-code-review[bot]reviewed2d ago
rogefmreviewed2d ago
rogefmreviewed2d ago
rogefm2d ago

Qodo findings 3 and 4 need no code change:

  • 3, verdict strings: each new case in TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto is named as the verdict an operator gets ("... is refused as the delete it runs", "... passes"), and asserts Denied and Operation through the postgres wire path, as sidecar/CLAUDE.md asks.
  • 4, audit rows: the before and after audit rows of every EXPLAIN and SELECT INTO statement are in Testing, item 1: the real hoop-inspect in front of Postgres, on main and on this branch. EVIDENCE.md has them too.
qodo-code-review[bot]2d ago

Qodo findings 3 and 4 need no code change:

  • 3, verdict strings: each new case in TestReadOnlyRuleRefusesWritesBehindExplainAndSelectInto is named as the verdict an operator gets ("... is refused as the delete it runs", "... passes"), and asserts Denied and Operation through the postgres wire path, as sidecar/CLAUDE.md asks.
  • 4, audit rows: the before and after audit rows of every EXPLAIN and SELECT INTO statement are in Testing, item 1: the real hoop-inspect in front of Postgres, on main and on this branch. EVIDENCE.md has them too.

The finding numbers are reversed in your summary:

  • finding 3 is “Audit evidence omits affected verdicts.” If the PR’s Testing item 1 and EVIDENCE.md contain the claimed before/after hoop-inspect audit rows for all affected statements, then this finding’s premise is no longer valid and no code change is needed. I could not find EVIDENCE.md in the checked-out PR files, so I cannot independently verify those rows here.
  • finding 4 is “Evasion cases lack verdict-string tests.” The test currently asserts SQL classification through inspectOne, Denied, and Operation (sidecar/inspect/bypass_test.go:414-477); those are not exact operator-facing verdict-string inputs. Therefore, the stated sidecar/CLAUDE.md testing guidance does not by itself address this finding, and the finding remains applicable unless the required verdict-string coverage exists elsewhere.

For the rule behind finding 4, its scope can be updated or disabled here: https://app.qodo.ai/rules/3062500?state=active

No dismissal was performed because you explained why the findings need no code change but did not explicitly ask to dismiss them.

qodo-code-review[bot]2d ago
rogefmreviewed2d ago
qodo-code-review[bot]reviewed2d ago
qodo-code-review[bot]2d ago
sandromello2d ago

✅ Build Completed with Success, Version=2087.0.0-g5b8842a

rogefmreviewed2d ago
rogefm2d ago

Right, the numbers moved when the review was updated. By title, neither needs a code change:

  • Audit evidence omits affected verdicts: the before and after audit rows are in Testing, items 1 and 3, and in EVIDENCE.md.
  • Evasion cases lack verdict-string tests: each case is named as the verdict an operator gets and asserts it (Denied and Operation) through the postgres wire path, as sidecar/CLAUDE.md asks.
qodo-code-review[bot]reviewed2d ago