pgxaip

package module
v0.0.0-...-563e070 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 13, 2026 License: MIT Imports: 9 Imported by: 0

README

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

Documentation

Overview

Package pgxaip rewrites AIP-160 filter, AIP-132 order_by, and keyset cursor values into Postgres SQL fragments that the caller splices into a query by hand.

The entry point is Query. Populate it from the request parsers (for example the ones emitted by protoc-gen-go-aip-query) and call Query.Rewrite to produce the WHERE predicate, the ORDER BY list, and the matching positional args. Pagination is offset- or cursor-based — driven by pagination.PageToken.Cursor when set — and the caller tacks the LIMIT/OFFSET pair onto the SQL themselves.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Query

type Query struct {
	Filter    filtering.Filter
	OrderBy   ordering.OrderBy
	PageToken pagination.PageToken
	Columns   map[string]string
}

Query bundles the parsed AIP inputs for a List request together with the AIP-path → DB-column allow-list that the rewriter uses to resolve identifiers. Any path in Filter or OrderBy that is not in Columns causes Query.Rewrite to fail.

OrderBy is the effective ordering — user-supplied fields plus any tiebreaker (the primary key, typically) the caller appends. When PageToken.Cursor is non-empty, its length must match len(OrderBy.Fields).

func (Query) Rewrite

func (q Query) Rewrite() (where, order string, args []any, err error)

Rewrite returns the SQL fragments and bound args derived from the query's filter, ordering, and cursor.

  • where is the WHERE predicate: the filter expression, the keyset cursor predicate, or the two ANDed together. Empty string when neither is present — the caller should omit the WHERE keyword.
  • order is the comma-separated ORDER BY list (no "ORDER BY" prefix). Empty string when OrderBy has no fields.
  • args are the positional bind values, numbered $1..$N. Filter literals come first, then cursor values. The caller appends its own LIMIT/OFFSET values at $N+1 and onward.

PageToken.Offset is not consulted; the caller feeds it into an OFFSET clause themselves.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL