parsepgx

package
v0.6.0 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: MIT Imports: 10 Imported by: 0

README

parse/parsepgx

Runs parse's plans on PostgreSQL through pgx. QueryPage answers a list plan — page, total, facets and includes — in two batches; Update applies an update plan and its relation changes in one transaction. The caller owns the context, the pool or transaction and its isolation; the core parse packages stay free of I/O.

Quick Start

func (a *deviceAPI) list(w http.ResponseWriter, r *http.Request) {
	tenant, err := a.tenant(r) // from the request's authentication
	if err != nil {
		http.Error(w, "unauthorized", http.StatusUnauthorized)
		return
	}
	req, err := devices.ParseList(r.URL.Query())
	if err != nil {
		a.fail(w, r, err)
		return
	}
	plan, err := devices.BuildList(filter.PostgresDialect{}, req, parse.Scope{Tenant: tenant.String()})
	if err != nil {
		a.fail(w, r, err)
		return
	}
	page, err := parsepgx.QueryPage(r.Context(), a.db, devices, plan)
	if err != nil {
		a.fail(w, r, err) // parsehttp.WriteError for parse errors, 500 otherwise
		return
	}
	a.reply(w, r, page)
}

The complete recipe — resource declaration, list and Merge Patch endpoints, error handling — is recipe_test.go, compiled in the unit lane and served against PostgreSQL by TestRecipe.

Architecture

sequenceDiagram
    participant H as HTTP handler
    participant R as parse.Resource
    participant Q as parsepgx.QueryPage
    participant PG as PostgreSQL (pgx pool or Tx)
    H->>R: ParseList(query), BuildList(dialect, req, Scope)
    R-->>H: ListPlan (Page, Count?, Facets?, Include)
    H->>Q: QueryPage(ctx, db, resource, plan)
    Q->>PG: batch 1: page, count, facets
    PG-->>Q: rows, total, facet rows (batch closed)
    Q->>R: Page(items, plan): cursors, has-next/prev
    Q->>PG: batch 2: one include read per relation, for the page's keys
    PG-->>Q: (owner, target) pairs
    Q-->>H: *filter.Page[T]: items, page_info, _relations, facets

Entry points

  • QueryPage(ctx, db, r, plan) — a list plan from Resource.BuildList, compiled for filter.PostgresDialect.
  • Update(ctx, db, r, plan) — an update plan from Resource.BuildUpdate.

Exit points

  • Include results pass through filter.Page.SetInclude: _relations and _truncated omit empty projections, and cut owners are reported with the retained IDs.
  • Statements to db through SendBatch (and Begin for an update that changes relations). Statement text and arguments come only from the plan.
  • Rows use pgx's db-tagged, embedded-field mapping. Columns omitted by parse.ReadColumns stay zero; declared JSON storage and JSON/JSONB result types use the checked JSON decoder described below.

Failure modes

  • A plan compiled for another dialect: ErrDialect, before any statement runs.
  • A statement error: the pgx/PostgreSQL error, wrapped with the table (parsepgx: devices: …); errors.As(err, &*pgconn.PgError) reaches it. In pgx's default mode a batch prepares every statement first, so a bad column fails there.
  • Invalid or lossy stored JSON: the decoding or key-validation error, wrapped with the table and read operation. No partial page or returned row slice is exposed.
  • A cursor value that cannot be minted from a row: Resource.Page's error.
  • A relation statement that fails inside Update: the transaction (or savepoint) rolls back the row change too.
  • Context cancelled or past its deadline: pgx's context error; nothing is retried.

Dependencies

  • Imports: parse, parse/core, parse/filter, released pkg/jsonvalue, github.com/jackc/pgx/v5
  • Imported by: applications serving parse resources on PostgreSQL
  • Package boundary: optional pgx execution; no application imports

Execution modes

The plans' arguments are typed ([]string, []uuid.UUID, float64, …), so every pgx QueryExecMode runs them; TestEveryExecMode runs a list walk and an update in each.

ConnConfig.DefaultQueryExecMode Use
QueryExecModeCacheStatement (default) Recommended: prepared once per connection, binary protocol
QueryExecModeCacheDescribe Behind PgBouncer in transaction mode with statement caching off
QueryExecModeDescribeExec No cache; one extra round trip per statement
QueryExecModeExec, QueryExecModeSimpleProtocol Poolers or proxies that cannot prepare; text encoding

pgx.NamedArgs rewrites whole statements and cannot compose with the plans' positional placeholders; it is not used.

JSON storage

Declared JSON columns and inferred PostgreSQL JSON/JSONB results decode as typed Go values: a stored JSON string becomes a Go string, and JSON-encoded bytes become the decoded byte slice. Native text, bytea and array columns retain pgx's normal codecs.

SQL NULL resets the destination field. A present JSON document decodes into a fresh candidate, with custom JSON decoders called once; canonical map keys and recognized struct-member names are checked before assignment. Lossy spellings such as {"01":1} into map[int]int are rejected. Custom JSON subtrees, interfaces and json.RawMessage retain their documented opaque representation.

A present JSON null cannot be represented by a nil map, slice, pointer or interface without losing its distinction from SQL NULL; use json.RawMessage to retain that document. Decoding and key-validation failures preserve the prior destination. Lists, empty-patch reads and update results use the same decoding boundary.

Consistency

The page, count and facet statements run in one batch, the includes in a second once the page's keys are known. On a pool each statement sees its own snapshot, so a concurrent write can make the total disagree with the page by a row. Pass a pgx.Tx begun with IsoLevel: pgx.RepeatableRead (read-only is enough) when the total, facets and includes must describe exactly the page's rows.

Update reads the owners the plan's scope admits and changes the relation memberships of those rows only, never the requested keys. A plan that writes nothing (plan.Writes false: an empty patch) only reads its targets; a caller that audits or fans hooks for every updated row checks plan.Writes before calling. With relation changes it runs in a transaction — a savepoint when db is already a pgx.Tx — so the caller's own transaction can still roll it back.

Operations

  • Timeouts: every statement takes the caller's context; give the request a deadline. Pool limits and lifetimes (MaxConns, MaxConnLifetime, MaxConnIdleTime, ConnConfig.ConnectTimeout) belong to the caller's pgxpool.Config.
  • Observability: the package emits no metrics or logs of its own. Instrument the pool with a pgx QueryTracer / BatchTracer (for example an OpenTelemetry tracer on ConnConfig.Tracer); each list is one or two batches, each update one batch, or two and a transaction.
  • Troubleshooting: an ErrDialect means the plan was built with another dialect; build it with filter.PostgresDialect{}. A 42703 (undefined column) means the table and T's db tags disagree. A list whose total looks one off under load is the per-statement snapshot; use a REPEATABLE READ transaction.

Security

The tenant, authorization policy and deleted-row visibility come only from the parse.Scope the server built the plan with; QueryPage and Update add no predicates and take none from the request. Includes read junction rows in the plan's tenant. SQL text comes from the plan, never from request strings, and every value is a bound argument.

API Reference

  • QueryPage[T](ctx, db Querier, r *parse.Resource[T], plan *parse.ListPlan) (*filter.Page[T], error) — the page with its total, facets (Page.Facets) and includes (Page.Relations) as the plan asks.
  • Update[T](ctx, db DB, r *parse.Resource[T], plan *parse.UpdatePlan) ([]T, error) — the owner rows as the update left them; none when no row in scope is a target.
  • Querier — SendBatch: *pgxpool.Pool, *pgx.Conn, pgx.Tx.
  • DB — Querier plus Begin: *pgxpool.Pool, *pgx.Conn, pgx.Tx (a savepoint).
  • ErrDialect — a plan not compiled for PostgreSQL.

Key Files

File Responsibility
parsepgx.go Querier, DB, QueryPage, Update, ErrDialect
json.go Native field mapping with fresh JSON decoding and canonical-key validation
json_integration_test.go Typed JSON lists/updates, nulls, projections and failure isolation in every execution mode
recipe_test.go The README recipe: Device, list and Merge Patch handlers, Example
recipe_integration_test.go TestRecipe: the recipe served against PostgreSQL
parsepgx_integration_test.go Page walks, tenancy, every exec mode, snapshots, updates and rollback

References

Documentation

Overview

Package parsepgx runs parse plans on PostgreSQL through pgx. QueryPage answers a list plan — the page, its total, its facets and its includes — and Update applies an update plan with its relation changes. Both take the caller's context, connection and transaction; the core parse packages stay free of I/O.

Index

Constants

This section is empty.

Variables

View Source
var ErrDialect = errors.New("parsepgx: plan is not compiled for PostgreSQL")

ErrDialect is a plan compiled for a dialect other than PostgreSQL.

Functions

func QueryPage

func QueryPage[T any](ctx context.Context, db Querier, r *parse.Resource[T], plan *parse.ListPlan) (*filter.Page[T], error)

QueryPage reads the page plan selects, scanned into T by its db tags (pgx.RowToStructByNameLax: fields of columns the resource does not read stay zero), and returns it as r.Page builds it: with its total when the plan counts, its facet counts when the plan facets, and the related IDs of the page's rows for each relation the plan includes.

The page, count and facet statements go to db in one batch, closed before the includes — which need the page's keys — go in a second. The statements run in their own snapshots unless db is a transaction at REPEATABLE READ or stricter; pass one when the total and facets must describe exactly the page's rows under concurrent writes.

func Update

func Update[T any](ctx context.Context, db DB, r *parse.Resource[T], plan *parse.UpdatePlan) ([]T, error)

Update applies plan and returns the rows its owner statement matched, as the update left them; none when no row in the plan's scope is a target. A plan that changes relation memberships runs in a transaction — a savepoint when db is one — so its rows and memberships change together: the owner statement first, then the membership changes for the keys it returned, never the requested ones. A plan that writes nothing (plan.Writes false: an empty patch) reads its targets; a caller that audits or notifies per updated row checks plan.Writes first.

Types

type DB

type DB interface {
	Querier
	Begin(ctx context.Context) (pgx.Tx, error)
}

DB sends batches and opens transactions: *pgxpool.Pool and *pgx.Conn begin a transaction, pgx.Tx a savepoint.

type Querier

type Querier interface {
	SendBatch(ctx context.Context, b *pgx.Batch) pgx.BatchResults
}

Querier sends statement batches: *pgxpool.Pool, *pgx.Conn and pgx.Tx have it.

Jump to

Keyboard shortcuts

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