Skip to content

Add union() and unionAll() to QueryGenerator - #18

Merged
danielbeardsley merged 5 commits into
masterfrom
add-union-support
Jul 27, 2026
Merged

Add union() and unionAll() to QueryGenerator#18
danielbeardsley merged 5 commits into
masterfrom
add-union-support

Conversation

@danielbeardsley

@danielbeardsley danielbeardsley commented Jul 27, 2026

Copy link
Copy Markdown
Member

Summary

Adds union() and unionAll() operators to QueryGenerator for combining SELECT statements. Operands may be another QueryGenerator (built and validated immediately) or a raw SQL string with params.

$qGen->select('id')->from('orders')->where('total > ?', 100);
$qGen->union($archivedQGen);
$qGen->unionAll('SELECT id FROM pending_orders');
$qGen->order('id');

Design notes

  • Implemented as real methods rather than via __call, because the existing nested-generator path builds sub-queries with skipClauses: true, which strips SELECT/FROM keywords — correct for WHERE fragments, wrong for union operands that must render as complete SELECTs.
  • The UNION / UNION ALL keyword is baked into each stored piece (glue stays "\n"), so the two can be mixed freely in call order.
  • The clause renders between HAVING and ORDER BY, so a trailing order()/limit() applies to the whole union result (MySQL semantics) and params merge in placeholder order.
  • QueryGenerator operands are validated at union() call time; opt out with skipValidation() on the operand.

qa_req 0 -- CI has new tests

CC @sterlinghirsh @sctice-ifixit

🤖 Generated with Claude Code

danielbeardsley and others added 2 commits July 27, 2026 11:36
Allow combining SELECT statements with UNION / UNION ALL. Operands may
be another QueryGenerator (built and validated immediately, keywords
intact) or a raw SQL string with params.

Implemented as real methods rather than via __call because the nested
generator path strips clause keywords (skipClauses), which would mangle
union operands. The UNION keyword is baked into each stored piece so
UNION and UNION ALL can be mixed in call order. The clause is rendered
between HAVING and ORDER BY so trailing ORDER BY/LIMIT apply to the
whole union and params merge in placeholder order.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@sctice-ifixit
sctice-ifixit self-requested a review July 27, 2026 19:02
djmetzle
djmetzle previously approved these changes Jul 27, 2026

@djmetzle djmetzle left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Neat. CR 🥢

sctice-ifixit
sctice-ifixit previously approved these changes Jul 27, 2026

@sctice-ifixit sctice-ifixit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I was worried about param ordering, but testUnionParamOrdering covers it. According to Claude: putting union between having and order in $methods means build() emits the union params in the right placeholder position, and the operand's own build() orders its params internally, so nesting works too.

One thing outside the diff that sent Claude down the wrong path: build()'s return annotation says the params come back as array<string, mixed>, which had it worried about named params colliding between the two halves of a union. As far as I can tell nothing in PopSQL uses named params (every example and test is positional ?) and build() returns a list, so it seems like that should be list<mixed>. Apparently Psalm sees this but emits it as Info rather than Error.

Comment thread QueryGenerator.php
Comment thread README.md
danielbeardsley and others added 3 commits July 27, 2026 14:00
A QueryGenerator operand carries its own params, so any passed alongside
were silently dropped. Make the mistake loud instead.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The return annotation claimed array<string, mixed>, suggesting named
params, but nothing in PopSQL uses them and params placeholders are
positional. Use array_values to make the list shape concrete.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@danielbeardsley
danielbeardsley dismissed stale reviews from sctice-ifixit and djmetzle via 7f39b6a July 27, 2026 21:01

@sctice-ifixit sctice-ifixit left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

CR 🌷

@danielbeardsley
danielbeardsley merged commit 9942645 into master Jul 27, 2026
4 checks passed
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.

3 participants