Skip to content

Fix doubled parentheses in Exists/Any/All with query operands - #743

Open
evanmarshall wants to merge 1 commit into
stephenafamo:mainfrom
evanmarshall:fix-exists-double-parens
Open

Fix doubled parentheses in Exists/Any/All with query operands#743
evanmarshall wants to merge 1 commit into
stephenafamo:mainfrom
evanmarshall:fix-exists-double-parens

Conversation

@evanmarshall

Copy link
Copy Markdown

Fixes #742

The bug

Exists, Any and All in expr/builder.go wrapped their operand in group{} unconditionally. But a bob.Query operand (such as the bob.BaseQuery returned by psql.Select/sqlite.Select/mysql.Select) already writes its own surrounding parentheses when rendered as an expression (bob.BaseQuery.WriteSQL). The two stacked into invalid SQL like EXISTS ((SELECT ...)), which fails to execute on SQLite and broke the generated relationship Where filters from #722 (Parents.R.HasEndpoints(...) etc., which return dialect.Exists(q)).

The fix

Exists/Any/All now skip the extra group{} when the operand implements bob.Query, relying on the query to write its own parentheses — the same assumption bob.BaseQuery.WriteSQL already makes for nested queries.

I deliberately did not reuse the ShouldOmitParens guard from X() as the issue first suggested: Arg, Raw, Clause etc. report ShouldOmitParens() == true but do not write their own parentheses, and EXISTS/ANY/ALL require parentheses around their operand — so routing through that check would have turned ANY ($1) into invalid ANY $1. Non-query operands keep the group exactly as before.

Also updated the doc comments in the three dialects' starters.go, which documented the doubled form (EXISTS ((SELECT 1))).

Tests

  • expr/builder_test.go: exact-string assertions that a query operand renders as EXISTS (SELECT ...) / ANY (...) / ALL (...) with a single set of parentheses, and that non-query operands (Raw, Arg) still get wrapped. Exact matching matters here because testutils.Clean collapses doubled parentheses, which would mask this regression.
  • dialect/sqlite/select_test.go: an end-to-end WHERE EXISTS (subquery) case validated by the SQLite grammar parser.

go test ./... passes except the pre-existing TestLibSQL container failure in gen/bobgen-sqlite/driver, which fails identically on a clean checkout of main (Docker/testcontainers environment issue, unrelated).

🤖 Generated with Claude Code

Exists, Any and All wrapped their operand in a group unconditionally,
but a bob.Query already writes its own surrounding parentheses when
rendered as an expression (see bob.BaseQuery.WriteSQL). The two stacked
into invalid SQL like EXISTS ((SELECT ...)), which fails on SQLite and
broke the generated relationship Where filters from stephenafamo#722.

Skip the extra group when the operand is a bob.Query. Non-query
operands (Arg, Raw, etc.) keep the group, since EXISTS/ANY/ALL require
parentheses around their operand.

Fixes stephenafamo#742

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Comment thread expr/builder.go
Comment on lines +70 to +80
// A [bob.Query] writes its own surrounding parentheses when rendered as an
// expression (see [bob.BaseQuery.WriteSQL]), so wrapping it in a group would
// produce doubled parentheses such as EXISTS ((SELECT ...)).
// Any other expression is wrapped in a group since EXISTS/ANY/ALL require
// parentheses around their operand.
func subquery(exp bob.Expression) bob.Expression {
if _, ok := exp.(bob.Query); ok {
return exp
}

return group{exp}

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This function name seems wrong... perhaps it should be named something likegroupIfNotQuery or something clearer

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.

Generated many-to-many Where filter emits invalid EXISTS ((SELECT ...)) with doubled parentheses

2 participants