[Domain] Orders search: id prefix and buyer-email substring - #243
Merged
Conversation
`OrderListFilter.search` matched an exact order id or an exact case-folded `buyer_ref`. The console never renders a full uuid — it renders the shortest unique prefix — so the one identifier on screen was the one identifier the list could not find, and an operator arriving with a fragment of a customer's email got nothing. Search now matches an order-id PREFIX or a `buyer_ref` SUBSTRING, ORed, with `lower()` applied to BOTH sides of both halves. Exact lookups are unchanged: a whole id is its own prefix, a whole address its own substring. The id half stays anchored — an unanchored match on hex would surface arbitrary rows. The fold is explicit because a bare LIKE is case-sensitive on Postgres and ASCII-case-insensitive on SQLite; only `lower(col) LIKE lower(:pattern)` makes the fake, SQLite and Postgres agree case for case. `%` and `_` stay literal (escaped pattern plus `ESCAPE '\'` in SQL; the fake builds no pattern at all). The sequential scan is the design, and the port now says so where it used to document the exact-match divergence from products. Measured over 5k orders: the predicate drops the old BitmapOr over `orders_pkey` + `idx_orders_buyer_ref_lower` for a seq scan, costing ~3 ms per page in the worst case. A trigram or full-text index at this scale buys operational surface, not latency. `OrderCustomerKey.buyerRef` deliberately did NOT follow: it answers whose orders these are, a substring would fold two customers into one history, and equality is what keeps the functional index on the plan. A contract case pins the difference. Every new case runs on the fake, SQLite and Postgres. The cursor gate compares the search STRING rather than what it selects, so the canonical form on the wire is unchanged — the admin Orders HTTP suite is untouched apart from added cases.
…emantics Three port/adapter comments described `OrderListFilter.search` as exact-only to explain their own choices. That is now false, and each of the three still has a real reason to differ, so the reasoning is restated rather than deleted: - Products: title and buyer_ref have CONVERGED on the substring; what remains different is `sku`, which stays exact because it is short and renders in full, where an order uuid renders only as a short prefix. - Coupons: the strictest search in the product, and now explicitly neither neighbour — a code is chosen, short, and quoted whole. - The products adapter's builder comment, matching its port. Comment text only; no behavior, no test change.
…he notes Review follow-up. Two behaviours the first pass left unpinned, and a set of comments that overstated what is true. Pins: - `\` is a LIKE metacharacter too, and the one the `%`/`_` cases cannot catch: unescaped, a search for `a\b` compiles to `%a\b%`, where `\b` means "a literal b" — it matches `ab@…` and misses the address that actually contains the backslash. Exactly inverted. Verified by removing the rule and watching the case fail with the inverted row. The same gap existed in the products suite and is closed there too. - The empty string matches EVERYTHING: every string starts with and contains `""`. That is the widest this axis goes, not the narrowest, and the naive reading is the opposite. The wire cannot send it (`min(1)`), so it is the port's own boundary. Corrections: - Migration `0022` no longer names order search among the functional index's consumers, and no longer promises that its test would catch a `LIKE` rewrite. What that test pins is three EXPLAINed statements; a predicate it does not EXPLAIN can drop off the index with nothing turning red, which is what just happened. - The port claimed dialect agreement "byte for byte". It inherits the repo's established accepted-divergence caveat instead: SQLite's `lower()` folds ASCII only where JS is Unicode-aware. The pattern-side fold moving from JS into SQL is stated rather than implied. - The measured timings are qualified as a floor — a synthetic four-column table, no `order_totals` join, and a searched page pays the predicate twice (list plus count). The products ~27 ms figure is marked as precedent for accepting a scan, not a bound on this one, and 3.359 ms is not rounded down. - A "newest-first" comment sat over a `.toSorted()` assertion; the assertion now makes the claim it was making. - The products contract's sku comment and one test title still described orders search as exact. - The changeset gains `@otta-sh/service` (its endpoint answers differently for the same query) and says why the plugin is not bumped; its opener now separates results preserved from plans changed. The three pending sibling changesets no longer describe orders search as exact-only, so the released changelog will not contradict itself.
The changeset said the explicit `lower()` makes the fake, SQLite and Postgres agree "case for case". They agree for ASCII. SQLite's built-in `lower()` folds ASCII only where JS `toLowerCase()` is Unicode-aware, so a non-ASCII buyer_ref folds differently on sqlite than on pg and the fake — the repo's existing accepted position, carried by `couponFilterConditions` and `linkGuestOrders` already and now by the port doc, inherited here rather than introduced. Emails and hex ids are ASCII, which is why it is accepted rather than solved. Also re-flows the two paragraphs left ragged by the earlier one-line corrections to the sibling changesets.
The port's cost note cited an internal tracking code for the exact filtered-set count. Replaced with the fact it stood for: a searched page issues the list and the count together, so a real page pays the search predicate twice under the same filter.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Admin orders search moves from exact-match-only to an order-id PREFIX combined, via OR, with a
case-folded buyer-reference SUBSTRING match.
%,_, and the escape character itself aretreated as literal characters — the LIKE pattern is escaped and the escaping is pinned by
contract cases, so a search string is never accidentally read as a wildcard. The exact-equality
customer-key lookup paths are deliberately left unchanged and keep their existing indices — a
substring there would fold distinct customers into one history.
The sequential scan this predicate now runs is the documented design, not an oversight: measured
as a floor on a synthetic harness, a searched page costs a list-plus-count pair. A b-tree cannot
serve an unanchored substring, and a trigram index at this scale buys operational surface, not
latency. The port doc, the migration rationale that used to describe the old exact-match index,
and the sibling changesets that referenced the old semantics were all re-pointed so no comment
anywhere still describes exact-only search.
Test plan
Contract-first: the new prefix/substring cases run against the in-memory fake, SQLite, and
Postgres by delegation through the shared store contract — 684 domain tests and 1078
store-postgres PG-tier tests green, including the wildcard, backslash-escaping, and
empty-string pins. The admin HTTP suite for orders is green with the cursor/filter gate
agreement unchanged. All Postgres-tier runs were against a local Postgres test instance.
Changeset included (
@otta-sh/domain+@otta-sh/store-postgresminor,@otta-sh/servicepatch).