016. Emit direct membership predicates for JSONB String lists

  • Status: Implemented
  • Date: 2026-10-14

Problem and mechanism

Membership on a JSONB list of strings can pass through several intermediate projections even when the binding declares a direct JSONB List<String> and all needles are literal Strings. For this narrow shape, the implementation emits JSONB containment directly in the filter:

("records"."tags" IS NOT NULL AND
  "records"."tags" @> jsonb_build_array(($1::text)))

Using @> can expose the predicate to PostgreSQL's JSONB GIN operator classes. The needles remain bound parameters rather than interpolated. With multiple needles, one JSON array is built and every requested string must occur in the bound list. Duplicate needles do not require duplicate list entries.

For example, tags:contains("auth","dev") tests whether both strings occur; the SQL uses one jsonb_build_array expression with two bound text values:

"records"."tags" @> jsonb_build_array(($1::text), ($2::text))

Applicability

Execution profile Status Reason
Direct physical JSONB List<String> binding with one or more literal String needles Applied Emits containment without projection barriers.
Nullable JSONB container with one or more literal String needles Applied The direct predicate includes a non-null test.
Null/dynamic needles, guards, metadata-backed or plain JSON, nested/Any/structural lists, or nullable elements Not applied These shapes retain the general lowering.

Reproduction

The comparison measures the one-needle HFF filter tags:contains("auth") and a direct physical binding of tags to the JSONB column records.tags, declared as List<String>. The baseline is commit a498cde; the implementation is commit c5d6fed. The code is in backends/sql/direct_jsonb_membership.go and its focused tests. The lengths below are generated WHERE expression character counts; barriers count OFFSET 0 projections in the expression. One bound text parameter is used in both forms.

For a plan-shape check, the direct predicate was prepared and explained against an empty temporary hff_candidate_tags table. Separate indexes using jsonb_ops and jsonb_path_ops were inspected with default planner settings.

Targeted unit and local/PostgreSQL parity checks passed for SQL NULL and JSON null inputs, empty lists, duplicate entries, Unicode and quoted/backslash strings, negation, conjunction/disjunction, and fallback observations. Full go test ./... and go vet ./... passed, and independent critical review found no material issue.

Results

HFF filter Prior WHERE length / barriers / parameters Direct WHERE length / barriers / parameters
tags:contains("auth") 1,164 characters / 4 / 1 84 characters / 0 / 1

Plain EXPLAIN selected a Bitmap Index Scan with either GIN operator class; the index condition was tags @> jsonb_build_array('auth'::text). This observation is limited to the empty temporary fixture and default planner settings. No EXPLAIN ANALYZE or execution timing was collected. The shorter SQL and this plan do not establish a runtime speedup.

Correctness and tradeoffs

The path is limited to a directly bound JSONB List<String> and one or more literal String needles. It preserves nullable-container filtering and leaves null needles, dynamic expressions, guards, metadata-backed values, plain JSON, nested or structural lists, Any elements, and nullable element types on the general lowering path. Targeted parity covers null distinctions, duplicate strings, Unicode, quoted/backslash content, boolean negation/composition, and representative fallback expressions.

Decision and limits

This implementation changes SQL construction only for the eligible list and needle shape. Whether @> uses a GIN index depends on the operator class, schema, statistics, and data. The fixture plan does not establish production index selection or execution performance. Validate generated SQL and plans against representative indexed tables before relying on index use.

Explore HFF
Search documentation

Search the language specification and guides.