Skip to Content

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 groupCost per rowCost 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.searchableEffect
null (default)string/text/char are searched; json/jsonb are not
trueForce-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
falseNever 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. getSearchTargets used 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 ILIKE instead of col::text ILIKE, so a future gin_trgm_ops index 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_ids entirely, along with its 150-key result cap.
  • Explicit column list in the RPC. daas_search_item_ids now takes p_columns TEXT[], so the caller passes the exact column list instead of the function re-deriving “everything text-ish”. Passing NULL keeps 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.

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 fields MCP tool
  • a direct daas_fields update

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 ilike predicate. 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 and meta.total reports the true count.
  • Opted-in json/jsonb columns are still matched by the daas_search_item_ids RPC, 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 text primary 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-footer and .collection-list-footer-pagination containers in collection-list.css.
  • Width Alignment: Set the sticky footer width to 100% and max-width: 100% with box-sizing: border-box to 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 high z-index so 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-list component dependency to 1.9.3.
  • Layout Correction: Changed .collection-list layout CSS rule from flex: 1 to flex: 0 1 auto to 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-dropdown fields in collection-list.tsx to display selected items as visual badges. It supports UUID values with tooltip truncation.
  • Value Normalization: Improved CollectionItemDropdown to 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 useEffect and added refs tracking to prevent infinite component re-renders.
  • Component Versions: collection-list and collection-item-dropdown are tracked at version 1.9.3 in buildpad.json. An earlier draft of this release bumped them to 2.0.0; that was reverted before shipping.
  • Wider value Prop: CollectionItemDropdownProps.value now 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.json still records the pristine upstream sha256, so buildpad upgrade reports the file as modified and will not silently overwrite it.
Last updated on