Skip to content

feat: add Query Builder whereColumn methods#10150

Open
memleakd wants to merge 4 commits intocodeigniter4:4.8from
memleakd:feat/query-builder-where-column
Open

feat: add Query Builder whereColumn methods#10150
memleakd wants to merge 4 commits intocodeigniter4:4.8from
memleakd:feat/query-builder-where-column

Conversation

@memleakd
Copy link
Copy Markdown
Contributor

@memleakd memleakd commented May 1, 2026

Description

This PR proposes adding whereColumn() and orWhereColumn() to the Query Builder for comparing one column to another column without dropping down to raw SQL.

This improves Query Builder DX for a common SQL pattern and reduces the need for manually written raw conditions like where('created_at < updated_at'). It also keeps the comparison inside the framework’s identifier-protection flow instead of asking users to handle that manually.

$builder->whereColumn('created_at', 'updated_at');
// WHERE "created_at" = "updated_at"

$builder->whereColumn('updated_at >', 'created_at');
// WHERE "updated_at" > "created_at"

$builder->orWhereColumn('published_at <=', 'expires_at');
// OR "published_at" <= "expires_at"

This is useful for common column-comparison queries while keeping the existing Query Builder behavior around identifier protection. The API follows the existing Query Builder convention of placing the operator in the first argument. If no operator is provided, = is used. Column names are protected by default, and $escape = false remains available for advanced cases such as SQL functions.

Supported operators are =, !=, <>, <, >, <=, and >=. Empty column names, unsupported operators, or ambiguous calls like whereColumn('created_at', '>=') throw an InvalidArgumentException.

Implementation note

This PR keeps the operator parsing local to whereColumn() so it does not change the long-standing behavior of where() / having(). There may be a future internal refactor opportunity to share operator parsing more broadly, but that is intentionally out of scope here to keep this PR focused and low-risk.

Changes

  • Added whereColumn() and orWhereColumn() to BaseBuilder.
  • Added Model PHPDoc forwarding entries.
  • Added user guide docs, class reference entries, and changelog entry.
  • Added tests for default operator, explicit operators, OR, grouping, alias/prefix protection, unescaped expressions, invalid columns, invalid operators, and ambiguous operator-as-second-column usage.

Checklist:

  • Securely signed commits
  • Component(s) with PHPDoc blocks, only if necessary or adds value (without duplication)
  • Unit testing, with >80% coverage
  • User guide updated
  • Conforms to style guide

- Add whereColumn() and orWhereColumn() for column-to-column comparisons
- Protect compared identifiers by default and support explicit unescaped usage
- Document supported operators and invalid argument behavior
- Add Query Builder, prefix, Model PHPDoc, user guide, and changelog coverage

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@github-actions github-actions Bot added the 4.8 PRs that target the `4.8` branch. label May 1, 2026
Copy link
Copy Markdown
Member

@michalsn michalsn left a comment

Choose a reason for hiding this comment

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

I like the idea, but I'm not particularly a fan of the implementation. And that's because of the $operator. The existing builder style puts the operator in the first argument, and if we create precedence here, some people may expect this is how it works everywhere.

To be fair, if I were designing this from scratch, I would probably prefer a separate operator parameter. But that is not the convention CodeIgniter has established, and I do not think this is the right place to introduce that inconsistency.

Also, in the current implementation, there is no way to distinguish "second column" from "operator with missing third argument", so whereColumn('created_at', '>=') becomes WHERE "created_at" = ">=".

@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 1, 2026

That makes sense. I agree that even if a separate operator parameter is nice in isolation, it is inconsistent with existing convention.

I’ll refactor this to parse the operator from the first argument, e.g. whereColumn('updated_at >', 'created_at'), default to =, and reject ambiguous cases like whereColumn('created_at', '>=').

@memleakd memleakd marked this pull request as draft May 1, 2026 05:42
- Parse comparison operators from the first column argument
- Reject ambiguous operator-as-second-column usage
- Update docs, examples, tests, and Model PHPDoc
- Keep operator parsing local to avoid changing existing where/having behavior

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
@memleakd
Copy link
Copy Markdown
Contributor Author

memleakd commented May 1, 2026

Thanks for the review and direction.

I pushed a refactor that aligns the API with the existing Query Builder convention and also updated the PR body.

@memleakd memleakd marked this pull request as ready for review May 1, 2026 07:48
Copy link
Copy Markdown
Contributor

@datamweb datamweb left a comment

Choose a reason for hiding this comment

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

Thank you for refactoring the code! I find the changes very useful and appreciate your effort.

Comment thread system/Database/BaseBuilder.php Outdated
Comment thread system/Database/BaseBuilder.php Outdated
- Treat the second argument as a normal column identifier
- Limit first-argument operator detection to terminal operators
- Add regression tests for identifier and expression edge cases
- Update docs for the refined validation behavior

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
Comment thread system/Database/BaseBuilder.php Outdated
- Detect only supported terminal comparison operators
- Default to equality when no supported operator is found
- Remove unsupported-operator validation
- Update tests and docs for the simpler behavior

Signed-off-by: memleakd <121398829+memleakd@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

4.8 PRs that target the `4.8` branch.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants