Skip to content

feat: Add wrapWhere method to group existing WHERE conditions#250

Merged
roxblnfk merged 1 commit into
2.xfrom
wrap-where
May 28, 2026
Merged

feat: Add wrapWhere method to group existing WHERE conditions#250
roxblnfk merged 1 commit into
2.xfrom
wrap-where

Conversation

@roxblnfk
Copy link
Copy Markdown
Member

@roxblnfk roxblnfk commented May 27, 2026

🔍 What was changed

Added wrapWhere() to WhereTrait. The method collapses all currently-registered WHERE tokens into a single nested AND-group, leaving the where state with exactly one top-level token — the enclosing group. Subsequent where()/andWhere()/orWhere()/... calls append at the top level alongside that group, no longer inside it.

$select
    ->where('a', 1)
    ->orWhere('a', 2)
    ->wrapWhere()      // freeze what came before into a group
    ->where('b', 3);
// WHERE (a = 1 OR a = 2) AND b = 3

Behavior:

  • No-op on empty where state — calling wrapWhere() before any condition is registered does nothing.
  • Idempotent for SQL semantics — repeated calls only add a redundant pair of parentheses; the resulting predicate is logically identical.
  • Order-preserving — internal token order and operators (AND, OR, NOT, BETWEEN, IN, closure groups, @or/@and array sugar) are kept as-is inside the new group.

Note

The method is a low-level primitive intended for use by ORM scope mechanisms (and any other code that needs to defensively enclose its conditions). It is safe to expose publicly but is not the canonical way to build a nested WHERE — for that, keep using where(function ($q) { ... }).

🤔 Why?

Today an ORM scope (e.g. SoftDeleteScope) adds its conditions at the top level of the WHERE clause. As soon as user code attaches orWhere(...) later, the scope and the user predicate join the same flat boolean chain:

WHERE deleted_at IS NULL OR id = 5
-- ^^ scope is silently bypassed

There was no way for a scope to enclose itself (or the user's prior conditions) in parentheses without a heavier rework — the only existing nesting primitive is the closure form of where(), which can only group conditions being added now, not conditions already registered.

wrapWhere() fills exactly that gap. With it, a scope's apply() can do:

public function apply(QueryBuilder $q): void
{
    $q->wrapWhere();                // protect anything already registered
    $q->where('deleted_at', null);  // add scope condition at the top level
}

producing

WHERE (user_conditions...) AND deleted_at IS NULL

This unlocks an isolation pattern for stacked scopes (e.g. SoftDelete + multi-tenancy + role visibility) without introducing a new scope interface, without changing ScopeInterface::apply(), and without touching the SelectQuery build pipeline — see the companion change in cycle/orm.

📝 Checklist

  • Closes #
  • How was this tested:
    • Tested manually
    • Unit tests added

Tests cover:

  • Token-level structure (3 unit tests in Tests/Unit/Query/Tokens/SelectQueryTest): no-op on empty state, exact token layout after wrapping a mixed AND/OR chain, scope-protection scenario.
  • SQL output across a range of combinations (11 new functional tests, inherited by every driver from Common/Query/SelectQueryTest): plain AND chain, mixed AND/OR, whereNot, wrapping an already-closure-nested group, attaching a closure group after wrapWhere(), double wrapWhere(), stacked scope emulation (two layers), orWhereNot after wrap, array syntax with @or, BETWEEN/IN operators, and a chain that starts with orWhere.

Full SQLite functional suite: 662 tests, no regressions.

📃 Documentation

Inline phpdoc on WhereTrait::wrapWhere() with a worked example and a description of the typical use case (scope isolation). No separate doc page is required for a single primitive; the consumer-facing documentation will live in the ORM-side PR that wires this into the scope mechanism.

@roxblnfk roxblnfk requested review from a team as code owners May 27, 2026 19:40
@github-actions github-actions Bot added the type: test Test label May 27, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented May 27, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 95.55%. Comparing base (395a7ff) to head (e0565f5).

Additional details and impacted files
@@            Coverage Diff            @@
##                2.x     #250   +/-   ##
=========================================
  Coverage     95.55%   95.55%           
- Complexity     2032     2034    +2     
=========================================
  Files           137      137           
  Lines          5755     5764    +9     
=========================================
+ Hits           5499     5508    +9     
  Misses          256      256           

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds a wrapWhere() primitive to WhereTrait that wraps all currently registered WHERE tokens in a single nested AND-group, so subsequent where/orWhere calls are appended alongside (not inside) the previous conditions. The primary motivation is enabling ORM scopes (e.g. soft-delete) to defend themselves against a later user-supplied orWhere that would otherwise break out of the intended boolean group.

Changes:

  • Add WhereTrait::wrapWhere() that prepends ['AND','('] and appends ['','')'] around existing where tokens (no-op when empty).
  • Add 3 unit tests covering token-layout (empty no-op, mixed AND/OR wrapping, scope-protection scenario).
  • Add 15 functional tests covering SQL output for various combinations (AND chains, mixed AND/OR, whereNot, closure groups before/after, double wrap, stacked scopes, orWhereNot, array @or, BETWEEN/IN, leading orWhere).

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated no comments.

File Description
src/Query/Traits/WhereTrait.php Introduces the wrapWhere() method that brackets existing where tokens into a single AND-group.
tests/Database/Unit/Query/Tokens/SelectQueryTest.php Token-level tests asserting exact structure after wrapWhere().
tests/Database/Functional/Driver/Common/Query/SelectQueryTest.php Driver-wide functional tests asserting compiled SQL across many condition shapes.

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

@roxblnfk roxblnfk merged commit 8f7c03a into 2.x May 28, 2026
31 of 32 checks passed
@roxblnfk roxblnfk deleted the wrap-where branch May 28, 2026 11:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants