BOGO Select for WooCommerce 2.3.6
Install: download bogo-select-2.3.6.zip below, then WordPress → Plugins → Add New → Upload Plugin.
[2.3.6] — 2026-08-02
The measurement, and what it turned out to be worth.
CODEX-REVIEW.md M-03 asked for a benchmark before any latency claim was
published. Running it found a broad gift search costing 612 database queries on
a 2,000-product catalogue; fixing what it found brought that to 15. The runtime
change is one batched fetch — behaviour is identical and only the cost moved,
which makes this a PATCH.
Changed
-
A page of gift choices is fetched in one batch instead of one product at a
time (CODEX-REVIEW.mdM-03). The benchmark below found a broad All
Products search costing 612 database queries against 2,000 products — about
three per candidate, because eachwc_get_product()found its own way to the
post row, its meta, and itsproduct_typeandproduct_visibilityterms.
2.3.1's request memo stops the same product being loaded twice inside a
request and does nothing about the request being the first one, which on a
store without a persistent object cache is every request._prime_post_caches()now asks for all of it once, before the eligibility
loop starts. Re-measured on the same catalogue: 612 queries → 15 for a
broad search, 1,508 → 11 for the curated list's cold eligibility build,
81 → 12 for browsing a page. A single-SKU search is unchanged at 12,
which is the guard working — a batch of one is skipped, since priming it
would cost a query to save none.Nothing downstream changed: the same products are loaded, by the same calls,
in the same order. This is not fewer loads, it is the same loads costing
fewer queries — which is why the unit suite's product-load counts are
untouched, and why the cost needed a benchmark to see at all.M-03 floated a result cache keyed by search term and catalogue state if
object reuse turned out not to be enough. This is the cheaper answer to the
same measurement: no new cache to invalidate, no key to get wrong, nothing to
go stale.
Added
-
A large-catalogue benchmark, and the numbers from running it
(CODEX-REVIEW.mdM-03). It asked for wall time, database queries, CPU, and
peak memory measured before any latency claim is published, and was explicit
that the product-load counts the unit suite holds are not latency. These are
seconds, queries, and bytes; the loads stay where they were.On 2,000 products with 500 curated, no persistent object cache: a broad All
Products search costs 0.23s and 612 queries cold and 0 queries warm; the
curated list's cold eligibility build costs 0.48s and 1,508 queries, carried
by a transient so a store pays it once per ten minutes rather than per
request; browsing a page costs 0.03s and 81 queries.The warm column is 2.3.1's request memo working — zero queries on the second
call, every path. The cold column was the finding: about three queries per
candidate, because the memo stops a product being loaded twice in a request
and does nothing about the request being the first. That is now fixed — see
below — and both sets of numbers are recorded inCODEX-REVIEW-RESPONSE.md
with what they do and do not say.Its own workflow, on
workflow_dispatch. Seeding takes about a minute and
the numbers are for reading rather than gating; a threshold on a shared runner
would mostly measure the runner. -
The settings screen is exercised through
options.phpunder a real role
(CODEX-REVIEW.mdL-02, and the regression test M-02 asked for).
AdminSettingsTest.phpcalls the same sanitize callback WordPress calls, and
structurally cannot reach what happens before it: the nonce, the option
allowlist, and the capability check. That check was the whole of M-02 — a Shop
Manager could fill the form in and be refused on submit.A Shop Manager now signs in, opens the page, saves a schedule, and the form is
read back to prove what was stored. An Editor is refused the page. A malformed
date and a reversed window are submitted and the previous schedule is shown to
have survived both — reading the form rather than the message, since a screen
that showed an error and stored the value anyway is exactly what M-01 was.The site runs on a non-UTC clock for it, and the dates come from the store's
owncurrent_time(), soDECISION.mdD-019's "whole days in the site's
timezone" is exercised rather than assumed: an offer ending today is not
called expired, and one ending yesterday is.