v0.1.88 — 27 Jul 2026
Changed — ?search= no longer scans json/jsonb columns by default
Who is affected: Anyone calling ?search= on a collection that contains a json or jsonb column — including anyone whose search was timing out on one.
Behaviour change. Content inside a json/jsonb column is no longer matched by ?search= unless that field opts back in with meta.searchable = true. This release also ships a migration that must be applied before the application deploy — see Deploy order.
What was wrong
Search ORed col::text ILIKE '%term%' across every text-ish and json column of the collection. Casting a json column to text detoasts and decompresses the whole value for every row scanned, and no index can serve that predicate.
Measured on a real claims collection whose json column averaged ~57 KB/row:
| Column group | Cost per row | Cost at 12k rows |
|---|---|---|
| The one json column | ~1.4 ms | ~17 s |
| All remaining text columns | ~0.06 ms | ~0.7 s |
The json column was ~95% of the scan cost. At ~17 s the query blew past the 8 s Postgres statement_timeout, so GET /api/items/claims?search=… returned HTTP 500 (canceling statement due to statement timeout).
What changed
A new daas_fields.searchable flag controls per-field participation in ?search=:
meta.searchable | Effect |
|---|---|
null (default) | string/text/char are searched; json/jsonb are not |
true | Force-includes a json/jsonb field. No effect on other non-text types (uuid, integer, date, boolean) — they have no ILIKE target and are never searched |
false | Never searched, whatever the type |
Supporting changes:
- Empty target list fails closed. If every field on a collection ends up excluded,
?search=now matches nothing. Previously no predicate was applied in that case, so the search silently returned the entire collection. - Unreadable metadata fails closed.
getSearchTargetsused to fall back to a hardcoded column guess (email,first_name,last_name,title,description,location), which returned a PostgREST 400 on any collection without those columns. It now returns no targets, so the search matches nothing instead of erroring. - Cheaper text predicate. Text and varchar columns now use a bare
col ILIKEinstead ofcol::text ILIKE, so a futuregin_trgm_opsindex on the plain column can serve them. - No RPC round-trip for text-only collections. Collections whose search takes the text-only path skip
daas_search_item_idsentirely, along with its 150-key result cap. - Explicit column list in the RPC.
daas_search_item_idsnow takesp_columns TEXT[], so the caller passes the exact column list instead of the function re-deriving “everything text-ish”. PassingNULLkeeps the old all-columns behaviour. The previous 4-argument signature is dropped rather than kept alongside the new one — a defaulted 5th parameter would have made 4-argument calls ambiguous.
Re-enabling json search
The flag is API-only in this release. The Data Model UI has no searchable control yet, so re-enabling json search — or excluding a noisy text column — is done through one of:
PATCH /api/fields/{collection}/{field}with{ "meta": { "searchable": true } }- the
fieldsMCP tool - a direct
daas_fieldsupdate
A UI toggle is still to come.
For exact-value lookups prefer a filter (?filter[claim_id][_eq]=…) over ?search=. A filter can use an index; ?search= cannot.
Deploy order
Apply supabase/migrations/20260726000001_field_searchable_metadata.sql before deploying the application code. Field metadata writes always include the new column, so app code running against a database without it fails every field and collection create with PGRST204.
Rollback is not symmetric. Revert the application deploy first, then drop the column — reverting only the migration is not a safe undo.
Fixed — Incomplete search results due to JSONB column RPC truncation
Who is affected: Developers and users searching collections with active query filters (e.g., searching with specific filters like hospital_id applied).
What was wrong
When searching on any collection containing at least one opted-in json/jsonb column, DaaS bypassed native PostgREST search and routed the whole query — text columns included — through the daas_search_item_ids RPC. The RPC matched every searchable column table-wide and returned only the first JSONB_SEARCH_MATCH_LIMIT primary keys (default 150), ordered by primary key, before any active query filter was applied.
For broad keywords (like "Konsultasi") that id list filled up with rows belonging to other entities, leaving no matching ids for the user’s filtered entity and producing 0 results. For more specific keywords (like "Konsultasi dokter") every match fit inside the cap, so results rendered correctly — which is what made the bug look intermittent.
What changed
Search in ItemsService (lib/services/items.ts) is now a hybrid rather than an either/or choice:
- Text/varchar columns are matched natively with a PostgREST
ilikepredicate. That path is uncapped and is ANDed with the active query filter by PostgREST, so a filtered search whose matches live in a text column is now correct andmeta.totalreports the true count. - Opted-in
json/jsonbcolumns are still matched by thedaas_search_item_idsRPC, which now receives only the json columns — the text columns no longer make the round trip. - Both results are ORed into a single filter group, so a row matches if it hits either side. A row matching in both is returned once.
The cap still applies to json/jsonb matches. The RPC selects its primary keys before the query filter runs, so a filtered search whose matches live only inside a json column can still under-report when more than JSONB_SEARCH_MATCH_LIMIT rows match the term table-wide. Prefer a real typed column for values you need to filter and search together.
Also fixed — search terms and primary keys containing , ( ) "
Folding both halves into one PostgREST filter group meant the search term and the RPC’s primary keys were being written into a filter string that PostgREST parses structurally. Neither was escaped for that grammar, which produced four distinct failures on collections with an opted-in json column:
- a term containing a comma (
?search=Konsultasi, dokter) returned HTTP 500 — PostgREST read the comma as a predicate separator and failed to parse the filter; - a term containing parentheses (
?search=Rawat (inap)) returned 200 with no rows, silently missing the match; - a term made only of grammar characters (
?search=,,,) returned the entire collection, because the fragment collapsed to a match-everything wildcard; - a
textprimary key containing a comma made its row disappear from json search results, because PostgREST split the one key into two.
Both values are now quoted and escaped when they are written into the filter (QueryBuilder.buildSearchFilter and the new QueryBuilder.buildPrimaryKeyInFilter). LIKE metacharacters (%, _, \) continue to be escaped so terms match literally.
This also closes a filter-injection path: a crafted text primary key could previously append an attacker-chosen predicate to the search group. A byte-identical dead copy of buildSearchFilter in lib/utils/query-params.ts was removed at the same time — it had no callers, and keeping it meant an escaping fix had to be applied twice to take effect.
Performance — readByQuery no longer requests an exact row count
Who is affected: Every read that goes through ItemsService.readByQuery, most visibly on large or searched collections.
What was wrong
readByQuery discards the row count, but every read still sent Prefer: count=exact, which costs a second full pass over the filtered set. On a searched table that pass is about as expensive as the search itself.
What changed
readByQuery now opts out of the count. readByQueryWithCount is unchanged, so GET /api/items/[collection] still returns meta.total.
Fixed — Table pagination style when horizontal scrollbar is active
Who is affected: Developers and users navigating Platform Studio collection tables on smaller screens or when tables have many columns.
What was wrong
When horizontal scrolling was active on the collection table, the bottom pagination/footer was not sticky, causing it to break out of alignment or disappear from view unless the user scrolled to the absolute bottom or right of the table container.
What changed
We resolved this by making the pagination footer layout sticky:
- Sticky Footer: Applied sticky positioning (
position: sticky,bottom: 0,left: 0) to both.collection-list-footerand.collection-list-footer-paginationcontainers incollection-list.css. - Width Alignment: Set the sticky footer width to
100%andmax-width: 100%withbox-sizing: border-boxto ensure it resizes correctly relative to the table container and does not overflow. - Background Styling: Ensured correct background layering with
background: var(--mantine-color-body)and a highz-indexso that the pagination footer remains readable over scrolling table rows.
Fixed — Table empty space layout issue
Who is affected: Developers and users navigating Platform Studio collection tables.
What was wrong
The collection list was taking up too much empty space inside the table component due to a rigid .collection-list layout styling (flex: 1).
What changed
We resolved this by upgrading the collection-list component and styling behavior:
- Component Upgrade: Upgraded
collection-listcomponent dependency to1.9.3. - Layout Correction: Changed
.collection-listlayout CSS rule fromflex: 1toflex: 0 1 autoto allow correct flexbox shrinking and spacing, resolving the excessive empty space.
Improved — Collection component rendering and dropdown handling
Who is affected: Developers and users configuring and using collection-item-dropdown interfaces in collection tables.
What was wrong
The collection list table was not rendering collection-item-dropdown fields correctly, showing unformatted raw values. Additionally, the dropdown component suffered from potential infinite loops due to object reference sensitivity, lack of input normalization (crashing on serialized JSON or raw keys), and lacked real API integration to load details for selected items.
What changed
We upgraded collection components and resolved rendering issues:
- Collection List Dropdown Support: Added a specialized renderer for
collection-item-dropdownfields incollection-list.tsxto display selected items as visual badges. It supports UUID values with tooltip truncation. - Value Normalization: Improved
CollectionItemDropdownto normalize primitive keys, JSON-serialized strings, and full item objects into a standard format, avoiding runtime crashes. - Optimized Data Fetching: Implemented API integration (
/api/items/...) to fetch display names for selected keys. It searches already-loaded local items first to minimize network requests. - Infinite Loop Prevention: Swapped raw value dependency arrays in
useEffectand added refs tracking to prevent infinite component re-renders. - Component Versions:
collection-listandcollection-item-dropdownare tracked at version1.9.3inbuildpad.json. An earlier draft of this release bumped them to2.0.0; that was reverted before shipping. - Wider
valueProp:CollectionItemDropdownProps.valuenow also accepts a raw key, a JSON string, or a resolved item object, matching what the normalizer already handled. This is a local modification to a Copy & Own component —buildpad.jsonstill records the pristine upstreamsha256, sobuildpad upgradereports the file asmodifiedand will not silently overwrite it.