Skip to content

Conversation

@joshheald
Copy link
Contributor

@joshheald joshheald commented Nov 20, 2025

Part of: WOOMOB-1112

Description

Adds a database search query method for searching products in the local GRDB catalog. This implements SQL LIKE queries to search by product name, SKU, and variations' SKU. The query excludes downloadable products and draft/trashed products, and orders results by product name.

This is the first PR in a stack of 5 PRs implementing local catalog search for WooCommerce POS.

Test Steps

No visible changes – this whole stack is probably best tested using #16377, the last one in the stack.

For this, just make sure the tests pass.

Note that #16377 resolves all the periphery issues – I'll merge them from that down, so the final merge to trunk will pass periphery. It seems silly to put a load of ignore statements in for the sake of this.


  • I have considered if this change warrants user-facing release notes and have added them to RELEASE-NOTES.txt if necessary.

joshheald and others added 11 commits November 19, 2025 10:03
Adds SQL LIKE-based search functionality to query local catalog products
by name, SKU, and global unique ID (GTIN). This enables local product
search without requiring remote API calls.

Changes:
- Add posProductSearch() method to PersistedProduct with LIKE queries
- Add escapeSQLLikePattern() helper to safely escape SQL wildcards
- Add comprehensive test suite with 17 test cases covering:
  - Search by name, SKU, and GTIN
  - Case-insensitive and partial matching
  - Filtering (downloadable products, product types)
  - Site isolation and result sorting
  - SQL special character escaping

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implements a new fetch strategy that searches the local GRDB catalog
instead of making remote API calls. The strategy will be integrated with
the factory pattern in a subsequent PR.

Changes:
- Add PointOfSaleLocalSearchPurchasableItemFetchStrategy
  - Searches local catalog using posProductSearch() query
  - Fetches PersistedProducts in single transaction
  - Converts to POSProduct outside transaction (avoids nesting)
  - Supports pagination with proper hasMorePages calculation
  - Delegates variations to remote (deferred to FTS)
  - Tracks analytics for first page results

- Add comprehensive test suite with 15 test cases:
  - Analytics tracking (first page only, subsequent pages)
  - Search by name, SKU, and global unique ID
  - Pagination edge cases and hasMorePages logic
  - Filtering (downloadable products, product types)
  - Site isolation and result sorting
  - Empty results handling
  - Variation delegation to remote

Implementation follows PointOfSaleLocalBarcodeScanService pattern for
safe GRDB transaction handling.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Replaces remote API calls with local GRDB queries for fetching product variations:

- Uses `posVariationsRequest()` to query variations from local catalog
- Implements pagination matching product search pattern
- Converts `PersistedProductVariation` to `POSProductVariation` with attributes/images
- Automatically filters downloadable variations (handled by query)
- Returns correct `PagedItems` with `hasMorePages` and `totalItems`

Adds comprehensive test coverage:
- Basic variation fetching from local catalog
- Downloadable variation filtering
- Empty results handling
- Pagination with multiple pages
- Parent product isolation

Analytics tracking will be added in final PR.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Integrates the local search strategy with the existing fetch strategy
factory pattern and wires it up to the POS UI. The factory automatically
chooses between local and remote search based on GRDB manager availability.

Changes:
- Update PointOfSaleItemFetchStrategyFactory:
  - Add optional grdbManager parameter to initializer
  - Add localSearchStrategy() factory method
  - Update searchStrategy() to prefer local when GRDB manager available
  - Falls back to remote search when local unavailable

- Update POSTabCoordinator:
  - Pass ServiceLocator.grdbManager to factory initialization
  - Enables local search when local catalog is available

- Update analytics infrastructure:
  - Add trackSearchLocalResultsFetchComplete() to protocol
  - Update mock analytics tracker for testing

Behavior:
- When GRDB manager is available → uses local catalog search
- When GRDB manager is nil → falls back to existing remote search
- No changes to UI layer (transparent integration)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Add isLocalCatalogEnabled parameter to factory init
- Replace implicit GRDB presence check with explicit flag check
- Remove side-effecty localSearchStrategy helper method
- Add stub implementation for trackSearchLocalResultsFetchComplete
- Pass isLocalCatalogEligible flag when creating factories in POSTabCoordinator
- Convert lazy factory properties to factory methods with eligibility parameter
- Makes intent clear and improves testability

The factory now explicitly checks the isLocalCatalogEnabled flag
rather than relying on the presence of GRDBManager to determine
which search strategy to use. This makes the behavior more
predictable and testable.

The POSTabCoordinator now dynamically creates factories with the
current eligibility state each time POS is presented, ensuring
the correct search strategy is used based on real-time eligibility.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
- Rename analytics event from pointOfSaleSearchLocalResultsFetched to pointOfSaleSearchResultsFetched
- Change stat name from search_local_results_fetched to search_results_fetched
- Add analytics tracking to local search strategy fetchProducts method
- Track milliseconds since request sent and total results count
- Only track on first page to avoid duplicate analytics

The new event name reflects that this will be the only search method
in the future when local catalog is fully rolled out.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Introduces a new enum to define different debouncing behaviors for search operations:

- `.smart(duration)`: Skip debounce on first keystroke after search completes, then debounce subsequent keystrokes. Optimized for slow network searches.
- `.simple(duration, loadingDelayThreshold?)`: Always debounce every keystroke. Optionally delays showing loading indicators until threshold is exceeded. Optimized for fast local searches.
- `.immediate`: No debouncing for non-search operations.

This allows different search types (local vs remote) to use appropriate debouncing strategies tailored to their performance characteristics.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Adds a `debounceStrategy` property to both PointOfSalePurchasableItemFetchStrategy and PointOfSaleCouponFetchStrategy protocols with default implementations returning `.immediate`.

This allows each fetch strategy implementation to declare its optimal debouncing behavior:
- Remote search strategies can use `.smart()` for network latency
- Local search strategies can use `.simple()` with loading delay thresholds
- Default strategies use `.immediate` for no debouncing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implements the debouncing strategy pattern in the search UI layer:

- Adds `debounceStrategy` property to POSSearchable protocol
- Updates POSSearchField's onChange handler to execute different debouncing logic based on strategy:
  - `.smart`: Skip debounce on first keystroke, debounce subsequent
  - `.simple`: Always debounce, with optional delayed loading indicators
  - `.immediate`: No debouncing
- Adds `currentDebounceStrategy` to item and coupon controllers to expose fetch strategy's debouncing behavior
- Implements protocol conformance in POSProductSearchable, POSOrderListView, and preview helpers

The `.simple` strategy with loading delay threshold prevents flicker on fast local searches by only showing loading indicators if the search exceeds the threshold duration.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Implements `.simple(duration: 150ms, loadingDelayThreshold: 300ms)` for local GRDB product searches.

This strategy:
- Always debounces every keystroke by 150ms to prevent excessive queries
- Delays showing loading indicators until 300ms threshold is exceeded
- Prevents loading flicker for fast local searches (< 300ms)
- Only shows loading indicators for slower searches (> 300ms)

The combination of debouncing with delayed loading provides a responsive feel while avoiding visual flickering on fast local database queries.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
Adds comprehensive test coverage for the debouncing strategy pattern:

- Tests for SearchDebounceStrategy enum equality
- Tests verifying each fetch strategy returns the correct debouncing behavior:
  - Local product search: `.simple` with loading delay threshold
  - Remote product search: `.smart` for network latency
  - Coupon search: `.smart` for network latency
  - Default strategies: `.immediate` for no debouncing

Tests ensure the debouncing strategies are correctly configured across all search types for optimal UX.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <[email protected]>
@joshheald joshheald added type: task An internally driven task. feature: POS labels Nov 20, 2025
@joshheald joshheald added this to the 23.8 milestone Nov 20, 2025
@wpmobilebot
Copy link
Collaborator

wpmobilebot commented Nov 20, 2025

App Icon📲 You can test the changes from this Pull Request in WooCommerce iOS Prototype by scanning the QR code below to install the corresponding build.

App NameWooCommerce iOS Prototype
Build Numberpr16373-a153e87
Version23.7
Bundle IDcom.automattic.alpha.woocommerce
Commita153e87
Installation URL6ls6fpigjod5g
Automatticians: You can use our internal self-serve MC tool to give yourself access to those builds if needed.

@joshheald joshheald marked this pull request as draft November 20, 2025 11:49
Copy link
Contributor

@iamgabrielma iamgabrielma left a comment

Choose a reason for hiding this comment

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

LGTM ✅

Comment on lines +147 to +148
.replacingOccurrences(of: "%", with: "\\%")
.replacingOccurrences(of: "_", with: "\\_")
Copy link
Contributor

Choose a reason for hiding this comment

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

Take it with a grain of salt since I'm not versed in this. The LLM shares that for completeness we can also escape \, but since GRDB passes the escape character explicitly and most product searches won’t include backslashes, the current implementation adequate.

If so, the suggestion would be to add .replacingOccurrences(of: "\\", with: "\\\\") as well.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm, interesting. I'll check this one, and see what the behaviour is if you search with slashes...

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, you're right, \ in the search term was treated as an escape character. Fixed in 992afaf

fullDescription: nil, shortDescription: nil, sku: nil, globalUniqueID: nil,
price: "45.00", downloadable: false, parentID: 0, manageStock: false,
stockQuantity: nil, stockStatusKey: "instock"),
PersistedProduct(id: 8, siteID: siteID, name: "Coffee Maker", productTypeKey: "variable",
Copy link
Contributor

Choose a reason for hiding this comment

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

Maybe I'd add a 4th product here that does not contain the searchTerm, so we're sure the #expect(results.count == 3) does not just return all products in the DB.

joshheald and others added 24 commits November 21, 2025 08:25
Co-authored-by: Gabriel Maldonado <[email protected]>
Added statusKey parameter to all PersistedProduct test objects
with value 'publish' to fix compilation errors after trunk merge.
Future PRs will handle other product statuses.
…ch' into woomob-1112-woo-poslocal-catalog-local-search-strategy
Co-authored-by: Gabriel Maldonado <[email protected]>
@dangermattic
Copy link
Collaborator

dangermattic commented Nov 24, 2025

2 Warnings
⚠️ View files have been modified, but no screenshot or video is included in the pull request. Consider adding some for clarity.
⚠️ This PR is larger than 300 lines of changes. Please consider splitting it into smaller PRs for easier and faster reviews.
1 Message
📖

This PR contains changes to Tracks-related logic. Please ensure (author and reviewer) the following are completed:

  • The tracks events must be validated in the Tracks system.
  • Verify the internal Tracks spreadsheet has also been updated.
  • Please consider registering any new events.
  • The PR must be assigned the category: tracks label.

Generated by 🚫 Danger

@joshheald joshheald added the category: tracks Related to analytics, including Tracks Events. label Nov 24, 2025
@joshheald joshheald enabled auto-merge November 24, 2025 17:48
@joshheald joshheald merged commit 4ee1f12 into trunk Nov 24, 2025
18 checks passed
@joshheald joshheald deleted the woomob-1112-woo-poslocal-catalog-implement-product-search branch November 24, 2025 18:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

category: tracks Related to analytics, including Tracks Events. feature: POS type: task An internally driven task.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants