List and Object Methods

The local backend implements the list and object method families for immutable HFF values. The complete signatures and language rules are in the method catalog; this guide focuses on useful query patterns.

List queries

count() returns the number of elements. Its predicate form counts elements whose callback result converts to true; filter() returns the original matching elements in their existing order:

scores:sum=12
scores:sum(score=>score!)=12
items:count
items:count(item=>item.active)=2
items:filter(item=>item.active):size>=1

For a schema-known List<Int|null> value [5, null, 7], both sum queries return 12. The second query demonstrates that a guarded no-value projection is skipped just like a null value.

Both predicate methods treat a guarded no-value callback result as false for that element. A null receiver yields null. filter() does not map or otherwise change retained values; filtering an empty list returns an empty list. The zero-argument count() requires method:list.count; the callback form and filter() also require the Callbacks capability. Nested callbacks require NestedCallbacks as well. A selected backend can still reject a query it cannot lower faithfully.

sum() accepts a numeric list or a callback that projects each element to a number. Null values and guarded no-value projections are skipped. An empty list or a list with no contributing values returns Int zero. A runtime nonnumeric contribution returns null; all-Int contributions are exact and overflow to null if their final total is outside int64. If any contribution is Float, the result is Float and follows the backend's declared Float model; a non-finite result is null. The zero-argument form requires method:list.sum; the callback form requires that method capability and Callbacks.

The PostgreSQL JSON/JSONB backend supports no-argument and callback sums over recoverable mixed numeric lists. For a List<Int|Float|null> binding, scores:sum returns Float for [1, 2.5]. A List<Any> binding can sum the same values when their runtime numeric subtypes are recoverable. Callback projections can also produce mixed or dynamic numeric contributions when those subtypes remain recoverable. For example, with scores bound as List<Int|Float|null>, values as List<Any>, and items as List<Struct{price:Int|Float|null}>:

scores:sum=3.5
scores:sum(item=>item)=3.5
values:sum=3.5
items:sum(item=>item.price)=3.5

Null contributions are skipped; a nonnumeric runtime value in values makes the result null. An empty or all-skipped list returns Int zero. If every contribution is Int, the final sum is exact when it fits int64. If any contribution is Float, the result is Float. For JSONB, provide version-1 numeric type metadata when stored numbers may have lost their Int/Float distinction. See the PostgreSQL JSON guide for the representation limits. PostgreSQL obtains round-trip-safe Float text for each contribution, accumulates those decimal values with SUM(numeric), and checks the final Float conversion. This requires extra_float_digits > 0; HFF does not set the database session value. Numeric accumulation can differ from Local's list-order Float additions.

tags:size>=2
tags:has(-1)
tags:contains("api", "production")
tags:unique
tags:sort=["api", "production"]
tags:concat(["internal"]):contains("internal")

size() returns the number of elements. has(index) accepts integer-valued numbers and supports negative indexes from the end; an out-of-range index is false and a non-integer index yields null. contains() requires one or more arguments and is true only when every argument occurs in the receiver. Both contains() and unique() use structural HFF value equality, so lists and objects compare by contents and numeric subtypes compare by numeric value.

sort() returns a stable sorted copy and does not modify its input. Elements must share an orderable type; mixed integer and float values are allowed. A runtime list with incompatible or non-orderable members produces null. The PostgreSQL backend also sorts Any and list-union JSON values when their stored element representations are recoverable; JSONB may need numeric type metadata. See the PostgreSQL JSON guide for the supported subset. concat(list) returns a new list containing the receiver followed by the argument.

distinct() is useful when a list contains repeated values but its original order matters. For tags: List<String|null> equal to ["api", "api", null, "prod", null] and numbers: List<Int|Float> equal to [1, 1.0, -0.0, 0.0]:

tags:distinct:json="[\"api\",null,\"prod\"]"
numbers:distinct:json="[1,-0.0]"

It keeps the first occurrence using structural HFF equality. Null appears once; numeric values with equal exact value deduplicate across Int and Float, while the first value's subtype and signed-zero representation are retained. The method requires method:list.distinct and any structural equality capabilities required by the possible element types.

Local unique() streams through the list and returns false at the first duplicate, so later elements are not examined by unique(). Fingerprint traversal and collision-confirming equality checks count against the per-match MaxSteps limit and observe cancellation; the boolean result does not consume MaxGeneratedValues. If either aborts, evaluation returns an error rather than a partial result. See the Local limits guide.

find(predicate) returns the original first matching element, which makes it convenient to select a record before reading one of its fields:

items:find(item=>item.active).id=2

For items: List<Struct{id:Int,active:Bool}> equal to [{"id":1,"active":false},{"id":2,"active":true}], Local evaluation stops at the second record and the query is true. Predicate results use ordinary boolean conversion; guarded no-value is false. Empty lists, no-match, and null receivers yield null. A matching null element also returns ordinary null. The method requires method:list.find and Callbacks; nested callback capability and depth limits apply when needed. Local supports both methods. PostgreSQL supports JSON/JSONB lists when the binding can recover the values and representations these operations need. Relations need a unique order because both operations observe the first occurrence. PostgreSQL preserves the result but does not execute callbacks with Local's procedural short-circuit or per-match work-budget behavior.

Selecting a numeric extreme

min() and max() select a numeric value directly or use a callback to project each element to a number:

readings:min
measurements:max(reading=>reading.temperature!)

For readings declared as List<Int|Float|null> with values [5, null, 7.5], the first query returns Int 5. If measurements contains structs with a nullable numeric temperature, the second query skips null and guarded no-value results. It returns the selected callback result with its original numeric subtype. A nonnumeric callback result makes the whole operation null. Empty or all-skipped lists also return null. Mixed Int and Float values are compared without pre-rounding. The first value wins a tie. A null receiver returns null.

PostgreSQL supports these methods for JSON/JSONB lists when stored values and callback results retain recoverable numeric subtypes. For example, with scores bound as List<Int|Float|null> and items as List<Struct{reading:Int|Float|null}>:

scores:min=5
scores:max=7.5
items:min(item=>item.reading!)=5
items:max(item=>item.reading!)=7.5

JSONB needs version-1 numeric type metadata when storage loses the Int/Float distinction. Any lists and dynamic callback results are supported when their runtime numeric subtype can be recovered. A nonnumeric runtime element or projection returns null; null and guarded no-value results are skipped. The first value wins an equal comparison, so ties retain both the original value and its numeric subtype.

Bound relations also support numeric extrema through callbacks. For example, events can be a relation whose items expose an integer priority:

events:min(event=>event.priority)=2

An Int-only or null-only relation projection does not need a unique order. Float or mixed Int/Float projections require a unique order because equal values can retain different signed-zero or subtype representations.

Averaging numeric values

Local avg() accepts numeric list elements or a callback that projects each element to a number:

temperatures:avg
measurements:avg(item=>item.temperature!)

For temperatures declared as List<Int|null> with values [2, 4, null], the first query returns Float 3. The callback form skips null and guarded no-value results as well. A nonnumeric runtime contribution, empty list, or all-skipped list returns null. The result is always Float. Local accumulates Int contributions exactly until a Float appears; then it adds using Go float64 in list order. An all-Int total converts to Float before division by the number of contributing values. A non-finite result returns null. A null receiver returns null.

For PostgreSQL schema-known JSON/JSONB lists, the same methods are supported when the binding preserves numeric subtypes. Float inputs use round-trip-safe Float text and AVG(numeric), followed by a checked conversion to Float. The backend requires extra_float_digits > 0; HFF does not set the session value. For example, if mixed is List<Int|Float|null> and items is List<Struct{price:Int|Float|null}>:

mixed:avg=1.75
items:avg(item=>item.price!)=1.75

These return Float for contributions [1, 2.5, null]. JSONB needs version-1 numeric type metadata when storage does not preserve the Int/Float distinction. PostgreSQL relation bindings support no-argument avg() when each item has a numeric Int or Float binding. For structured items, use a callback projection when the selected field has a recoverable numeric type. Neither form requires a unique relation order unless a nested operation observes order. For example, an unordered scalar relation and an unordered relation with a numeric price field can use:

measurements:avg=12.5
items:avg(item=>item.price)=12.5

Float aggregate profiles

Float aggregates follow each backend's declared Float model, so the same Float list can produce different predicate results. With items declared as List<Float> and bound to JSON or JSONB in PostgreSQL, compare these queries across Local and PostgreSQL:

items:sum>0
items:avg:exists

For [1e16, 1.0, -1e16], Local evaluates items:sum>0 as false because list-order Float addition loses the middle 1.0; PostgreSQL evaluates it as true because its numeric intermediate retains the contribution. For [1e308, 1e308], Local's average becomes null after the Float sum overflows, so items:avg:exists is false. PostgreSQL's numeric average produces a finite Float, so the same query is true. PostgreSQL Float aggregates require PostgreSQL 16 or later and extra_float_digits > 0; HFF does not change the database session setting. See the PostgreSQL JSON collection details for binding limits. The compatibility Profile guide shows how to retrieve the float.aggregation detail from a built filter or SQL query. The runnable aggregate Profile example prints the Local and PostgreSQL details; run it with go run ./examples/aggregate-profiles.

The Profile describes backend behavior; it does not make Float arithmetic identical across backends.

Object queries

labels:size=2
labels:has("team")
labels:get("team")=null
labels:keys=["app", "team"]
labels:values=["auth", null]
labels:items=[["app", "auth"], ["team", null]]

has(key) checks whether the key exists, including when its value is null. get(key) returns null for a missing key, so use has() when the distinction between a missing key and a stored null matters. keys(), values(), and items() enumerate keys in Unicode code-point order. Each item is a two-element [key, value] list, and the values and items use the same order as the keys.

PostgreSQL collection filters

The PostgreSQL backend can answer some root collection filters without decoding or aggregating the complete value:

tags=[]
tags:size=0
tags:size>0
labels={}
items=[]
items
items:size=0

For an unguarded, statically typed root JSONB binding without type metadata, empty list/object comparisons and bare collection truth use the physical JSONB value. Nonzero literal size() comparisons use a guarded jsonb_array_length for a List. SQL NULL and JSON null remain false as filter receivers; nonzero Map/Struct sizes retain general lowering. A plain relation can use a correlated existence check or a state/constant test for empty list comparisons, truthiness, and zero-cardinality comparisons; higher cardinality thresholds retain the count-based lowering. Nested JSON paths, metadata-backed bindings, guards, computed receivers, and dynamic values use general lowering. See the PostgreSQL backend guide for binding details and the translation optimization notes for the exact scope.

Dynamic values and capabilities

The compiler checks capabilities for every method family that can apply to a static union. At runtime, the receiver's actual family selects the method implementation; an incompatible dynamic receiver yields null. An explicit type guard can narrow a call to one family:

value:[list[string]]:contains("api")

The guard preserves a matching list and produces internal no-value for a different runtime type. This is useful with an Any field or union. Capability selection happens during compilation; local backend support is checked by local.Build.

Collection results are newly allocated and the input values remain immutable. Local per-match MaxSteps and MaxGeneratedValues limits cover traversal and generated collection containers and entries. Cancellation or a limit error aborts evaluation without returning a partial match. See the local evaluation guide for backend configuration and error handling.

Run the complete example from the repository root:

go run ./examples/collections

It prints:

tags:contains("api","production") labels:has("team") -> true
tags:sort=["api","production"] -> true
labels:keys=["app","team"] -> true
Explore HFF
Search documentation

Search the language specification and guides.