Skip to content

pgx-contrib/pgxaip

Repository files navigation

pgxaip

CI Release Go Reference License Go

pgxaip rewrites a parsed AIP-160 filter, an AIP-132 order_by, and an optional keyset cursor into Postgres SQL fragments you splice into a query by hand.

query := pgxaip.Query{
    Filter:    filter,     // from filtering.ParseFilter
    OrderBy:   orderBy,    // from ordering.ParseOrderBy
    PageToken: pageToken,  // from pagination.ParsePageToken (incl. Cursor)
    Columns:   columns,    // AIP path -> DB column
}

where, order, args, err := query.Rewrite()
  • where — the WHERE predicate (filter, cursor, or both ANDed). Empty when neither is present.
  • order — the col ASC, col DESC list, no ORDER BY prefix. Empty when OrderBy has no fields.
  • args — positional bind values, numbered $1..$N. Filter literals first, then cursor values. Append your own LIMIT / OFFSET at $N+1.

PageToken.Offset is not consulted by Rewrite; feed it into your OFFSET clause yourself.

Installation

go get github.com/pgx-contrib/pgxaip

Usage

filter, _ := filtering.ParseFilter(req, BookFilterDeclarations)
orderBy, _ := ordering.ParseOrderBy(req)
pageToken, _ := pagination.ParsePageToken(req)

// Unified column map: union of the generated *FilterColumns and
// *OrderByColumns. Same path -> same column by construction, so the
// union is safe.
columns := map[string]string{}
for k, v := range BookFilterColumns {
    columns[k] = v
}
for k, v := range BookOrderByColumns {
    columns[k] = v
}

q := pgxaip.Query{
    Filter:    filter,
    OrderBy:   orderBy,
    PageToken: pageToken,
    Columns:   columns,
}
where, order, args, err := q.Rewrite()

Columns is the AIP-path → DB-column allow-list. Lookup is fail-closed: any filter / order / cursor path that is not in Columns causes Rewrite to return an error, so an unmapped field can never leak into generated SQL. Pair with the *FilterColumns / *OrderByColumns maps produced by protoc-gen-go-aip-query or hand-build a map[string]string for ad-hoc use.

Filter operators

AIP filter Postgres fragment
=, !=, <, <=, >, >= col op $N (or col op col)
AND, OR (lhs AND rhs) / (lhs OR rhs)
NOT (NOT expr)
name:"ali" "name" ILIKE '%' || $N || '%'
timestamp("2025-01-02T03:04:05Z") $N bound as time.Time
duration("1h30m") $N bound as time.Duration
unary -<literal> bound as signed numeric literal

Cursor pagination

When PageToken.Cursor is populated, Rewrite emits the standard compound keyset predicate from OrderBy.Fields:

("name" < $1)
  OR ("name" = $1 AND "id" > $2)

Direction per field follows the OrderBy field's Desc flag (ASC → >, DESC → <). len(PageToken.Cursor) must equal len(OrderBy.Fields); mismatch is a validation error.

For a stable ordering, append a tiebreaker (the PK) to OrderBy.Fields before calling Rewrite, and make sure PageToken.Cursor carries a matching trailing value.

On the first page (PageToken.Cursor is empty) the cursor predicate is omitted.

Development

nix develop
go tool ginkgo run -r

License

MIT

About

Drop-in pgx v5 query rewriter that turns AIP-160 filter and AIP-132 order-by strings into parameterized WHERE and ORDER BY clauses.

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors