fix: Scope orWhere clauses in Item::exists() and Item::get_item_id() to fix items performance regression by jekkos · Pull Request #4520 · opensourcepos/opensourcepos · GitHub
Skip to content

fix: Scope orWhere clauses in Item::exists() and Item::get_item_id() to fix items performance regression#4520

Merged
jekkos merged 1 commit into
masterfrom
fix/items-performance-regression
Apr 20, 2026
Merged

fix: Scope orWhere clauses in Item::exists() and Item::get_item_id() to fix items performance regression#4520
jekkos merged 1 commit into
masterfrom
fix/items-performance-regression

Conversation

@jekkos

@jekkos jekkos commented Apr 19, 2026

Copy link
Copy Markdown
Member

Summary

  • Fixes unscoped orWhere clauses in Item::exists() and Item::get_id() introduced in PR Fix item number lookup in sales/receivings (#4212) #4250 (commit 29c3c55) that cause a performance regression and incorrect query semantics in the items view
  • Wraps orWhere conditions in groupStart()/groupEnd() to generate correct SQL parenthesization

Problem

PR #4250 added orWhere('item_number', $item_id) to exists() and orWhere('item_id', $item_number) to get_item_id() to support barcode lookup. However, these OR conditions were not scoped with parentheses, causing two issues:

1. Wrong SQL Semantics

Before fix (broken):

WHERE item_id = ? OR item_number = ? AND deleted = 0

Due to SQL operator precedence (AND binds tighter than OR), the deleted = 0 filter only applies to the item_number branch. This means deleted items can match via item_id, returning extra rows and causing getNumRows() === 1 checks to fail for legitimate items.

After fix (correct):

WHERE (item_id = ? OR item_number = ?) AND deleted = 0

2. Performance Regression

The unscoped OR causes MySQL's optimizer to bypass the item_id primary key index and fall back to less efficient query plans, particularly when comparing a string item_number column against a numeric parameter. This results in full table scans or index scans on large tables.

Additionally, get_item_id() unnecessarily JOINs the suppliers table even when only checking item existence, adding further overhead.

Testing

  • Verified the generated SQL with groupStart()/groupEnd() produces correct parenthesized queries
  • Both exists() and get_item_id() are fixed

Related

Summary by CodeRabbit

Bug Fixes

  • Fixed item search and filter queries to correctly process multiple item identifiers, resulting in more accurate and reliable results when locating items in the system.

In PR #4250 (commit 29c3c55), orWhere was added to match items by
either item_id or item_number, but the OR condition was not wrapped
in groupStart()/groupEnd(). This causes:

1. Wrong SQL semantics: generates
   WHERE item_id = ? OR item_number = ? AND deleted = 0
   instead of
   WHERE (item_id = ? OR item_number = ?) AND deleted = 0
   Due to AND binding tighter than OR, the deleted filter only applies
   to the item_number branch, allowing deleted items to match via item_id.

2. Performance: the unscoped OR causes MySQL to bypass the item_id
   primary key index and fall back to full table scans when item_number
   is a string column compared against a numeric parameter.

Both exists() and get_item_id() are fixed by wrapping the OR
conditions in groupStart()/groupEnd() for proper parenthesization.
@coderabbitai

coderabbitai Bot commented Apr 19, 2026

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot 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.

🧹 Nitpick comments (1)
app/Models/Item.php (1)

393-402: Grouping fix is correct; consider also dropping the unused suppliers JOIN.

The groupStart()/groupEnd() wrap around the OR conditions is correct and matches the fix in exists().

However, per the PR description, get_item_id() also incurs "an unnecessary JOIN on suppliers when only checking item existence". The query selects nothing from suppliers and the WHERE clause doesn't reference it, so the left join at line 394 is pure overhead on what's meant to be a hot lookup path (e.g., called via Sales.phpsale_lib->get_item_id()). Removing it would complete the performance-regression fix hinted at in the PR description.

♻️ Proposed follow-up
     public function get_item_id(string $item_number, bool $ignore_deleted = false, bool $deleted = false): bool|int
     {
         $builder = $this->db->table('items');
-        $builder->join('suppliers', 'suppliers.person_id = items.supplier_id', 'left');
         $builder->groupStart();
         $builder->where('item_number', $item_number);
         $builder->orWhere('item_id', $item_number);
         $builder->groupEnd();
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Models/Item.php` around lines 393 - 402, The left JOIN on suppliers is
unnecessary in get_item_id() and should be removed to avoid the extra overhead;
locate the get_item_id method (the code creating $builder =
$this->db->table('items')), delete or skip the $builder->join('suppliers',
'suppliers.person_id = items.supplier_id', 'left') call, confirm no columns from
suppliers are referenced elsewhere in that method (the groupStart()/groupEnd()
and where/orWhere logic should remain), and run tests or the hot lookup path
(sale_lib->get_item_id) to verify behavior is unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@app/Models/Item.php`:
- Around line 393-402: The left JOIN on suppliers is unnecessary in
get_item_id() and should be removed to avoid the extra overhead; locate the
get_item_id method (the code creating $builder = $this->db->table('items')),
delete or skip the $builder->join('suppliers', 'suppliers.person_id =
items.supplier_id', 'left') call, confirm no columns from suppliers are
referenced elsewhere in that method (the groupStart()/groupEnd() and
where/orWhere logic should remain), and run tests or the hot lookup path
(sale_lib->get_item_id) to verify behavior is unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d629a2c0-8468-4711-9ef1-ca5875eb2771

📥 Commits

Reviewing files that changed from the base of the PR and between 12e3c7e and aeaed30.

📒 Files selected for processing (1)
  • app/Models/Item.php

@jekkos jekkos merged commit e602edd into master Apr 20, 2026
14 checks passed
@objecttothis objecttothis deleted the fix/items-performance-regression branch May 19, 2026 12:03
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.

2 participants