Filter Many-to-Many Memberships Through a View

BindRelation takes one table or view as its row source. To expose a many-to-many relationship, make the join explicit in a PostgreSQL view and bind the view rows as an HFF list.

Tables, view, and sample rows

CREATE TABLE users (id bigint PRIMARY KEY, name text NOT NULL);
CREATE TABLE groups (id bigint PRIMARY KEY, slug text NOT NULL UNIQUE);
CREATE TABLE memberships (
    user_id bigint NOT NULL REFERENCES users(id),
    group_id bigint NOT NULL REFERENCES groups(id),
    PRIMARY KEY (user_id, group_id)
);
CREATE VIEW user_group_rows AS
SELECT m.user_id, g.id AS group_id, g.slug
FROM memberships AS m
JOIN groups AS g ON g.id = m.group_id;

INSERT INTO users VALUES (1, 'Ada'), (2, 'Lin'), (3, 'Sam');
INSERT INTO groups VALUES (10, 'platform'), (20, 'security');
INSERT INTO memberships VALUES (1, 10), (2, 10), (2, 20), (3, 20);

The logical query shape is users.groups[], with each item containing the group ID and slug. Users 1 and 2 belong to platform; user 3 does not. For example, user 2 is reconstructed as:

{"groups":[{"id":10,"slug":"platform"},{"id":20,"slug":"security"}]}

Bind the view rows

func platformUsers(ctx context.Context, db *sql.DB) ([]int64, error) {
	groupType := schema.Struct(schema.Int("id"), schema.String("slug"))
	e, err := engine.New(
		engine.WithSchema(schema.Struct(schema.List("groups", groupType))),
		engine.WithCapabilities(capabilities.Standard()),
	)
	if err != nil {
		return nil, err
	}
	program, err := e.Compile(ctx, `groups:any(group=>group.slug="platform")`)
	if err != nil {
		return nil, err
	}
	relation, err := hffsql.NewRelation(
		hffsql.WithRelationSource(hffsql.Column("user_group_rows"), "group_row"),
		hffsql.WithCorrelation(hffsql.Equal(
			hffsql.ColumnValue(hffsql.Column("group_row", "user_id"), schema.IntType),
			hffsql.ColumnValue(hffsql.Column("users", "id"), schema.IntType),
		)),
		hffsql.WithRelationItem(groupType,
			hffsql.BindStruct(hff.Path{}, hffsql.Boolean(true), hffsql.Boolean(false),
				hffsql.Bind(hff.Path{"id"}, hffsql.Column("group_row", "group_id")),
				hffsql.Bind(hff.Path{"slug"}, hffsql.Column("group_row", "slug")),
			),
		),
	)
	if err != nil {
		return nil, err
	}
	query, err := hffsql.Build(ctx, program,
		hffsql.WithDialect(hffsql.PostgreSQL),
		hffsql.WithBindings(hffsql.BindRelation(hff.Path{"groups"}, relation)),
	)
	if err != nil {
		return nil, err
	}
	rows, err := db.QueryContext(ctx,
		`SELECT users.id FROM users 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
}

The function imports context, database/sql, and the HFF hff, hffsql, capabilities, engine, and schema packages. The service executes the complete parent query and passes query.Args() separately. For the sample rows, the result is []int64{1, 2}.

The view is the complete relation source; HFF does not generate its joins or discover membership tables. Add a typed unique order only if the HFF query observes the list order, such as by indexing or returning the collection. See the relation shape guide for view and nested-relation alternatives.

Explore HFF
Search documentation

Search the language specification and guides.