Search Dynamic JSON Labels

Search services whose team label is either a string or a list of strings. The same filter handles both shapes and returns matching service IDs.

CREATE TABLE services (
    id bigint PRIMARY KEY,
    labels jsonb NOT NULL
);

INSERT INTO services VALUES
    (1, '{"team":"platform","environment":"production"}'),
    (2, '{"team":["platform","on-call"],"environment":"production"}'),
    (3, '{"team":"internal","environment":"production"}'),
    (4, '{"team":["platform"],"environment":"staging"}');

Declare the union in the schema, then bind the JSONB column at the root HFF path labels. Build translates the predicate; the caller executes it with the returned SQL fragment and bound arguments:

package main

import (
	"context"
	"database/sql"

	"github.com/ornew/hff"
	hffsql "github.com/ornew/hff/backends/sql"
	"github.com/ornew/hff/capabilities"
	"github.com/ornew/hff/engine"
	"github.com/ornew/hff/schema"
)

func filterServices(ctx context.Context, db *sql.DB) ([]int64, error) {
	teamType := schema.Union(
		schema.StringType,
		schema.ListOf(schema.StringType),
	)
	e, err := engine.New(
		engine.WithSchema(schema.Struct(schema.Map("labels", teamType))),
		engine.WithCapabilities(capabilities.Standard()),
	)
	if err != nil {
		return nil, err
	}

	program, err := e.Compile(ctx,
		`labels.team:contains("platform") labels.environment="production"`)
	if err != nil {
		return nil, err
	}
	query, err := hffsql.Build(ctx, program,
		hffsql.WithDialect(hffsql.PostgreSQL),
		hffsql.WithBindings(hffsql.BindJSON(
			hff.Path{"labels"},
			hffsql.Column("services", "labels"),
			hffsql.JSONB,
		)),
	)
	if err != nil {
		return nil, err
	}

	rows, err := db.QueryContext(ctx,
		`SELECT services.id FROM services WHERE `+query.Where(), query.Args()...)
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	var ids []int64
	for rows.Next() {
		var id int64
		if err := rows.Scan(&id); err != nil {
			return nil, err
		}
		ids = append(ids, id)
	}
	if err := rows.Err(); err != nil {
		return nil, err
	}
	return ids, nil
}

Executing this predicate against the sample table selects IDs 1 and 2. The string form uses substring matching; the list form searches for an equal string element. ID 3 has another team, and ID 4 has a non-production environment. Because this schema allows only strings and lists of strings, this filter does not need numeric JSONB metadata. The runnable translator example builds the same filter without opening a database:

go run ./examples/sql-label-search

Arguments are evaluated left to right. A failed containment check does not skip later arguments. Null string receivers or reached null substring arguments propagate null; a list contains() can search for a stored null only when the declared list element type permits null (for example, a nullable element or Any). See the PostgreSQL JSON guide for dynamic dispatch, in() and unique() support, and null/no-value rules.

Explore HFF
Search documentation

Search the language specification and guides.