Bind PostgreSQL Data to HFF Records
This tutorial connects a relational table to the logical record shape expected by an HFF filter. The SQL backend builds a predicate; your application still chooses the parent query and executes it.
The Go fragments below share these imports and form the body of a function that receives the application's already configured database handle:
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 filterOrders(db *sql.DB) error {
ctx := context.Background()
Start with the physical tables
An order has related item rows. The foreign key is part of the application's database schema; HFF does not discover it:
CREATE TABLE orders (
id bigint PRIMARY KEY,
status text NOT NULL,
tenant_id bigint NOT NULL
);
CREATE TABLE order_items (
id bigint PRIMARY KEY,
order_id bigint NOT NULL REFERENCES orders(id),
price bigint NOT NULL,
active boolean NOT NULL
);
INSERT INTO orders VALUES
(42, 'open', 7),
(43, 'open', 7),
(44, 'open', 8);
INSERT INTO order_items VALUES
(7, 42, 125, true),
(8, 42, 35, false),
(9, 43, 125, false),
(10, 43, 35, true);
Declare the logical HFF record
The filter sees one record with an items list, even though PostgreSQL stores
the items in another table. The HFF equivalent records are:
order 42: matches
{"items":[{"price":125,"active":true},{"price":35,"active":false}]}
order 43: no single item satisfies both callback conditions
{"items":[{"price":125,"active":false},{"price":35,"active":true}]}
order 44: no matching child rows, so items is an empty list
{"items":[]}
Declare that shape in Go and compile a filter against it:
item := schema.Struct(schema.Int("price"), schema.Bool("active"))
e, err := engine.New(
engine.WithSchema(schema.Struct(schema.List("items", item))),
engine.WithCapabilities(capabilities.Standard()),
)
if err != nil { return err }
program, err := e.Compile(ctx,
`items:any(item=>item.price>100 item.active)`,
)
if err != nil { return err }
The first record matches: one item costs more than 100 and is active. The second does not, because those conditions hold on different items. The third does not because its list is empty. The caller's query below therefore returns only order 42.
Bind the list to correlated rows
BindRelation attaches the physical child rows to the logical items path.
The relation item's empty path means the row currently named item is the
HFF list item; its child binding paths are relative to that row:
itemBinding := hffsql.BindStruct(
hff.Path{},
hffsql.Boolean(true),
hffsql.Boolean(false),
hffsql.Bind(hff.Path{"price"}, hffsql.Column("item", "price")),
hffsql.Bind(hff.Path{"active"}, hffsql.Column("item", "active")),
)
parentOrder := hffsql.Equal(
hffsql.ColumnValue(hffsql.Column("item", "order_id"), schema.IntType),
hffsql.ColumnValue(hffsql.Column("orders", "id"), schema.IntType),
)
tenantMatch := hffsql.Equal(
hffsql.ColumnValue(hffsql.Column("orders", "tenant_id"), schema.IntType),
hffsql.Literal(hff.Int(7)),
)
relation, err := hffsql.NewRelation(
hffsql.WithRelationSource(hffsql.Column("order_items"), "item"),
hffsql.WithCorrelation(hffsql.And(parentOrder, tenantMatch)),
hffsql.WithRelationItem(item, itemBinding),
)
if err != nil { return err }
query, err := hffsql.Build(ctx, program,
hffsql.WithDialect(hffsql.PostgreSQL),
hffsql.WithBindings(hffsql.BindRelation(
hff.Path{"items"}, relation,
)),
)
if err != nil { return err }
There are two different paths here. hff.Path{"items"} is the root HFF
field. hff.Path{} inside BindStruct is the item root, so
hff.Path{"price"} is one item field. A path such as
hff.Path{"items", "price"} cannot address the current item; it would try
to bind a child beneath the list. SQL identifiers are separate:
Column("item", "price") refers to the row alias and database column, not an
HFF field path.
Execute the predicate in your query
Build returns only the predicate and its ordered arguments. The service
chooses the parent table and uses the same alias referenced by the relation
correlation. Assume these snippets are inside a function with ctx and an
already-open *sql.DB; the application chooses and configures its driver.
The tenant filter is part of the DDL above, and parentOrder is the typed
foreign-key correlation built in the previous block:
rows, err := db.QueryContext(ctx,
`SELECT "orders"."id" FROM "orders" WHERE `+query.Where(),
query.Args()...,
)
if err != nil { return err }
defer rows.Close()
var ids []int64
for rows.Next() {
var id int64
if err := rows.Scan(&id); err != nil { return err }
ids = append(ids, id)
}
if err := rows.Err(); err != nil { return err }
// ids contains only 42.
return nil
}
Pass Args() to the database driver as parameters; do not interpolate them
into SQL. The SQL backend does not connect to PostgreSQL, execute this
statement, or validate the table definitions or row data. The application
owns the schema contract, connection, transaction, timeout, and result scan.
The relation uses typed Equal, And, Literal, and ColumnValue
constructors for its correlation; column identifiers are quoted and their
declared HFF types must match. The tenant condition contributes a bound
argument. For predicates a typed builder cannot express, TrustedSQL is a raw
service-authored escape hatch. HFF does not parse or validate its syntax,
meaning, purity, or portability. Its ColumnRef arguments only support alias
bookkeeping. Any raw expression used for HFF state or correlation must be
deterministic and side-effect-free; HFF cannot prove that property. Keep such
expressions in reviewed service code, never concatenate query values or user
input into them, and keep HFF query literals bound through Args().
Choose the physical representation
Use the binding that matches how the value is stored:
Bind(path, Column(...))maps one scalar field to one SQL column. SQLNULLis an explicit HFF null.BindJSON(path, column, JSONB)maps a schema-known object, map, or list stored in one JSON/JSONB document. Nested fields live inside that document.BindComposite(path, column)maps one struct to a native PostgreSQL composite. SQL-null composite and null attributes remain distinct; composite attributes cannot represent a missing field.BindStruct(path, present, null, fields...)maps one object spread over columns. The caller supplies separate predicates for parent presence and whether the reconstructed HFF value is logically null. A flattened parent has no single SQL value whose nullness can be inferred; do not derive its state from all-null child columns.BindRelation(path, relation)maps a list to correlated child rows. No rows mean an empty list by default, not a missing or null list.
One-to-many relations map naturally to lists. A nested one-to-many relation is
another explicit BindRelation on an item field. The current relation source
is one table or view plus a correlation predicate; HFF does not generate a
many-to-many join or turn a relation into a one-to-one struct. Expose a
many-to-many row shape through a view or model the junction and nested
collection as separate relations. For full SQL examples and the exact null,
missing, and ordering contracts, see the
PostgreSQL backend guide and its
relation bindings,
JSON/JSONB,
composite, and
flattened struct guides.
The runnable builder example uses the same relation setup without needing a database:
go run ./examples/sql-relations
Filter native bytea values
The same scalar binding pattern maps a logical HFF Bytes field to a
PostgreSQL bytea column. bytes:len() counts bytes, bytes:base64() encodes
standard padded base64, and bytes:base64r() encodes standard base64 without
padding. string:frombase64() and string:frombase64r() strictly decode their
respective forms.
The decoded result can be compared directly with a bound bytea field.
CREATE TABLE binary_assets (
id bigint PRIMARY KEY,
payload bytea NOT NULL,
encoded text NOT NULL
);
INSERT INTO binary_assets VALUES
(1, decode('01020304', 'hex'), 'AQIDBA=='),
(2, decode('010203', 'hex'), 'AQIDBA=='),
(3, decode('01020304', 'hex'), 'AQIDBA');
Compile a filter over the stored bytes and their textual encoding:
func filterBinaryAssets(db *sql.DB) ([]int64, error) {
ctx := context.Background()
e, err := engine.New(
engine.WithSchema(schema.Struct(
schema.Bytes("payload"),
schema.String("encoded"),
)),
engine.WithCapabilities(capabilities.Standard()),
)
if err != nil {
return nil, err
}
program, err := e.Compile(ctx,
`payload:len=4 payload:base64=encoded encoded:frombase64=payload`,
)
if err != nil {
return nil, err
}
query, err := hffsql.Build(ctx, program,
hffsql.WithDialect(hffsql.PostgreSQL),
hffsql.WithBindings(
hffsql.Bind(hff.Path{"payload"}, hffsql.Column("binary_assets", "payload")),
hffsql.Bind(hff.Path{"encoded"}, hffsql.Column("binary_assets", "encoded")),
),
)
if err != nil {
return nil, err
}
rows, err := db.QueryContext(ctx,
`SELECT binary_assets.id FROM binary_assets 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 // Contains only row 1 for the sample data above.
}
This function uses the imports and HFF APIs shown at the start of the tutorial;
the caller supplies an initialized database handle. Only row 1 matches. Row 2
has the wrong byte length, and row 3 has unpadded, invalid base64. Invalid
decoding yields null, so it does not match the byte comparison.
For raw unpadded standard base64, use frombase64r() instead; the padded
decoder deliberately rejects row 3.
To build the same predicate without a database connection, run
go run ./examples/sql-bytes. It prints whether predicate construction
succeeded and how many bound parameters it produced.
PostgreSQL bytea is the supported stored representation for HFF Bytes.
JSON and JSONB strings can be decoded explicitly with frombase64(), but JSON
collections cannot store HFF Bytes values. The backend builds a predicate and
does not execute it or inspect table types; the application passes Where()
and Args() to its database driver.
Continue with other SQL shapes
The flattened-profile recipe shows how explicit parent-state columns preserve missing, null, and present struct values. The nested-relations recipe correlates child rows at two levels. For a JSONB document with numeric type metadata, see JSONB numeric thresholds; for many-to-many membership, see a relation over a view.
Filter with a literal regular expression
The PostgreSQL backend can translate a literal pattern such as ^A on
PostgreSQL 15 and later. This table shows how the HFF predicate corresponds to
the physical text column:
CREATE TABLE contacts (
id bigint PRIMARY KEY,
name text NOT NULL
);
INSERT INTO contacts VALUES (1, 'Alice'), (2, 'Bob'), (3, 'Ava');
Compile and build name~"^A" with the same schema and binding pattern used
above:
e, err := engine.New(
engine.WithSchema(schema.Struct(schema.String("name"))),
engine.WithCapabilities(capabilities.Standard()),
)
if err != nil {
return err
}
program, err := e.Compile(ctx, `name~"^A"`)
if err != nil {
return err
}
query, err := hffsql.Build(ctx, program,
hffsql.WithDialect(hffsql.PostgreSQL),
hffsql.WithBindings(hffsql.Bind(
hff.Path{"name"}, hffsql.Column("contacts", "name"),
)),
)
if err != nil {
return err
}
Use query.Where() and query.Args() in the caller's SELECT; the sample
predicate selects contacts 1 and 3. The backend accepts only the documented
literal RE2 subset. A non-literal or well-formed but unsupported pattern fails
at Build; a malformed pattern evaluates to null. See the
regex subset and limitations.