Found on main @ 9c496fa, verified by execution.
The select builder infers its keyword the same way the write builders did before #1113 — by scanning the statement text (client.ts:3350):
const kw = SQL_PATTERNS.WHERE.test(text) ? 'AND' : 'WHERE'That branch is only reached once a set operator has closed the current SELECT (whereTail). At that point text holds the left SELECT — including its WHERE — plus the operator plus the right SELECT. So the scan answers "does the left side have a WHERE?" when the question is "does the right side have one?"
Repro
db.selectFrom('a').where('y', '=', 2).union(db.selectFrom('b')).where('x', '=', 1).toSQL()Emitted:
SELECT * FROM a WHERE y = $1 UNION SELECT * FROM b AND x = $2
-- ^^^ should be WHEREWhich does not parse.
Without the leading where, the same chain is fine, because the scan finds nothing:
SELECT * FROM a UNION SELECT * FROM b WHERE x = $1And when the right side genuinely has its own predicate, AND is correct and is what comes out:
SELECT * FROM a UNION SELECT * FROM b WHERE y = $1 AND x = $2So the bug is precisely: a predicate on the left side is mistaken for one on the right.
Severity
Lower than #1113. This is a parse error, so it is loud — there is no silent-damage path here the way there was for UPDATE ... SET x = (…) AND id = ?. It just means "filter the right-hand side of a union" is unavailable to anyone who also filtered the left.
Suggested fix
Same shape as #1113, but the state has to describe the tail: record at appendSetOp time whether the right-hand SELECT already carries a predicate, and key the keyword off that. The right-hand operand is usually one of our own builders, so it can be asked directly rather than scanned; the text scan is only a fallback for a foreign { toSQL }.
Noticed while fixing #1113 (PR #1118) — left out of that PR because it is a different builder and a different fix.