Skip to content

Latest commit

 

History

History
214 lines (187 loc) · 13.9 KB

File metadata and controls

214 lines (187 loc) · 13.9 KB

Feature support

Checklist of PostgreSQL and GraphQL features pdbq supports. Checked = implemented and tested today; unchecked = planned or explicitly deferred (tier noted where one applies: v1.x = next minor releases, v2 = major-version track, stretch = nice-to-have). Use this as the reference when adding features: tick the box in the same PR that ships it.

Introspection (pg_catalog)

  • Tables (including partitioned tables)
  • Views (read-only)
  • Materialized views (read-only)
  • Columns: type, nullability, defaults, identity/generated detection
  • Primary keys
  • Unique constraints
  • Foreign keys (including multi-column)
  • Indexes (name, columns, uniqueness, method) — drives the filter policy
  • Enums
  • Functions (args, return type, set-returning, volatility)
  • Comments on tables, columns, enums, functions (exposed as GraphQL descriptions), and constraints (smart comments)
  • RLS-enabled status per table
  • Granted privileges per table (SELECT/INSERT/UPDATE/DELETE gate generation)
  • Schema allowlist (schema.schemas)
  • Composite types read into the catalog
  • Composite-type columns mapped to GraphQL object types (matching <Type>Input on mutations; composite arrays included)
  • Domains surfaced as named scalars (currently resolved to base type) (v1.x)
  • Function argument default values / variadic args (v1.x)
  • Functions with OUT/INOUT parameters or anonymous record returns (v1.x)

Type mapping (Postgres → GraphQL)

  • int2/int4Int
  • int8BigInt (string-serialized, no precision loss)
  • float4/float8Float
  • numeric/moneyBigFloat (string-serialized)
  • boolBoolean
  • text/varchar/char/citext/nameString
  • uuidUUID
  • json/jsonbJSON
  • timestamp/timestamptzDatetime
  • dateDate, time/timetzTime
  • byteaString (base64 output)
  • Arrays of any mapped type → GraphQL lists
  • Enums → GraphQL enum types (labels mapped to spec-valid names, round-tripped on input and output)
  • inet/cidr/macaddr, interval, ranges, ltree, tsvectorString fallback
  • interval as structured scalar (v1.x)
  • Range types with structured bounds (v1.x; range filter operators tracked under Filtering & ordering)
  • hstore (v1.x)
  • ltree operators (v1.x)
  • Full-text search: tsvector/tsquery filter ops (v1.x)
  • PostGIS geometry/geographyGeoJSON scalar (GeoJSON out; GeoJSON or WKT/EWKT in), with spatial filters, derived accessors, projection arguments and distance ordering — see PostGIS

Queries

  • Relay connection per table (allUsers): nodes, edges { cursor node }, totalCount, pageInfo, with first/last/offset/before/after
  • Keyset (index-backed) cursors — cursor = nodeId (base64(["Type", pk...])); PK-less tables fall back to offset cursors
  • Node interface / global object identification (nodeId: ID!, Relay node(nodeId:))
  • Single-row lookup by primary key (userById)
  • Single-row lookup per unique constraint (userByEmail)
  • Forward relations (FK → parent object field)
  • Backward relations (FK → child connection field with full connection args)
  • Arbitrarily nested relation selections, compiled to one SQL statement (lateral joins, no N+1)
  • Field aliases and __typename
  • Named + inline fragments
  • GraphQL variables with coercion and default values
  • Multiple root fields per operation
  • Depth limit and cost limit per operation
  • Aggregate fields (count/sum/avg on lists) — stretch
  • GraphQL subscriptions / live queries — non-goal for v1 (hook surfaces designed with this in mind; ship later)

Filtering & ordering

  • <Type>Filter input per table with and / or / not combinators, arbitrarily nested
  • Scalar operators: equalTo notEqualTo in notIn isNull lessThan lessThanOrEqualTo greaterThan greaterThanOrEqualTo
  • Text operators: like likeInsensitive startsWith endsWith (operands escaped)
  • Array operators: contains containedBy overlaps
  • jsonb operators: contains containedBy containsKey pathExists pathMatch
  • Enum filtering with GraphQL enum values
  • Indexed-only policy (default): only indexed/PK/unique columns filterable and orderable
  • Per-table column allowlist override (filters.allow_columns)
  • Global policy switch (filters.indexed_only: false)
  • orderBy enums (EMAIL_ASC/EMAIL_DESC), multi-column, PK tiebreaker always appended
  • Filters/ordering on backward relation fields, not just root lists
  • jsonb path operators (@?/@@ via pathExists/pathMatch)
  • Range operators (&&, @> element, <<, >>) (v1.x, alongside structured range types)
  • Filtering across relations (posts: {some: {title: {equalTo: ...}}}) — built-in advanced-filters plugin: forward FK takes the parent's filter, reverse FK a {some|none|every} wrapper, compiled to EXISTS subqueries
  • Filtering on relation existence (postsExist: true, authorExists: false) — Boolean variant per relation, compiled to a bare (NOT) EXISTS
  • Filtering/ordering on computed columns — built-in advanced-filters plugin (single-row-argument stable functions; not index-backed)
  • distinctOn (column-enum arg on connections → SELECT DISTINCT ON; distinct-aware totalCount; first/offset pagination only)

Mutations

  • create<Type>(input:) per insertable table
  • update<Type>ByPk(patch:) per updatable table with PK
  • delete<Type>ByPk per deletable table with PK
  • Payload types with the mutated row selectable (relations included, compiled against the DML CTE)
  • Generated/identity columns excluded from inputs; defaulted columns optional
  • Privilege-gated generation (no INSERT grant → no create mutation)
  • Not-found update/delete → GraphQL error, transaction rolled back
  • Nested mutations via built-in plugin: create/connect of FK parents, nested create of children, multi-CTE single statement, bounded depth, forced transaction
  • Update/delete by unique constraints (updateUserByEmail, deleteUserByEmail, mirroring the lookup surface)
  • Upsert (ON CONFLICT) — upsert<Type>By<Unique>(input:) per non-generated key target; provided non-key columns update from EXCLUDED
  • Bulk mutations (createUsers(input: [...]), updateUsers(filter:, patch:), deleteUsers(filter:); payload = mutated rows + affectedCount)
  • clientMutationId passthrough (Relay classic)
  • Nested connect on reverse relations and nested update/delete/disconnect (v1.x)

Functions as fields

  • Stable/immutable functions → Query fields
  • Volatile functions → Mutation fields
  • Scalar arguments mapped from GraphQL args (named args only)
  • Scalar returns (via to_jsonb)
  • SETOF <table> returns → list of the table's object type with full selection support
  • SETOF <scalar> returns → scalar lists
  • Single-row table returns with selection support
  • void returns → Boolean
  • EXECUTE-privilege gating
  • Computed columns (stable/immutable functions whose first argument is a row type → fields on that type; extra scalar args become field args; set-returning/volatile deferred)
  • Set-returning computed columns (SETOF <scalar>/SETOF <table> row-type functions → list fields with full selection support)
  • Functions with table-valued arguments or polymorphic types (v1.x)
  • Custom mutations returning payload types with relations (v1.x)

RLS & auth

  • On by default; per-operation SET LOCAL ROLE (never plain SET on pooled connections)
  • Claims via transaction-scoped set_config('pdbq.claims.*', ...)
  • JWT claim source (HS256/384/512, issuer/audience checks)
  • Trusted-header claim source for behind-gateway deployments
  • Anonymous role for unauthenticated requests
  • Role claim configurable; default role fallback
  • Role name validation before interpolation into SET ROLE
  • rls.enabled: false escape hatch with loud startup warning
  • JWKS / RS256 asymmetric JWT verification (rls.auth.jwks_url + jwks_cache_ttl; RS256/384/512 + ES256/384/512, kid-matched, rotation-aware cache)
  • Per-request role allowlist (rls.allowed_roles; default/anonymous roles always allowed)
  • Claim → GraphQL context exposure for plugins beyond op.Claims (exec.OperationFromContext/ClaimsFromContext — the operation rides the request context into CompileHooks)

Transactions & execution

  • Every mutation in a transaction by default (transactions.mutations)
  • One transaction per operation; mutations abort remaining root fields on error
  • Queries transaction-free unless RLS context requires one
  • Plugins can force a transaction (op.ForceTx)
  • Isolation level config (read_committed / repeatable_read / serializable)
  • Statement timeout per connection (database.statement_timeout)
  • PG error → GraphQL error mapping with errors.detail: dev|prod (constraint violations pass through in prod, internals sanitized)
  • One transaction per request spanning operations (transactions.per_request)
  • Automatic retry on serialization failures (transactions.max_retries; SQLSTATE 40001/40P01, whole-operation re-run)
  • Savepoints for partial mutation recovery (v1.x)

Server & protocol

  • POST /graphql (JSON body) and GET /graphql (query params)
  • GraphQL introspection (__schema, __type, __typename), resolved in-process and exempt from depth/cost limits
  • Embedded GraphiQL playground (opt-in, off by default)
  • GET /healthz / /readyz
  • GET /schema.graphql (SDL export; opt-in, off by default)
  • Request timeout, max body size
  • Atomic schema hot-swap (in-flight requests finish on the old schema)
  • /metrics (Prometheus) endpoint (v1.x)
  • Persisted queries / APQ (server.apq Apollo protocol, server.persisted_queries_path allowlist file, server.persisted_only lockdown)
  • @defer / @stream (v2)
  • Schema contracts — named schema variants that hide tagged types/fields per audience (e.g. public/partner/internal), each with its own SDL export and served endpoint; builds on the @omit/tag machinery in smart-comments (v2)
  • CORS configuration (server.cors_origins: exact-match allowlist or *)
  • Response compression (gzip, opt-in via server.compression)

Schema cache & watch mode

  • pdbq schema dump — versioned, hashed, gzipped catalog snapshot
  • serve --schema.cache_path — boot without touching pg_catalog
  • pdbq schema check — CI drift gate (non-zero exit on drift)
  • Format-version and corruption rejection on load
  • Watch mode: DDL event trigger + LISTEN/NOTIFY re-introspection
  • Poll-hash fallback when event triggers can't be installed
  • Watch + cache combination rejected at config validation
  • Drift diff detail in schema check output (per-object added/removed/changed lines, column-level for tables)

CLI & config

  • pdbq serve|query|schema|config|plugins subcommands
  • pdbq query pipe-friendly: stdin queries, --var k=v (JSON-parsed), --vars-file -, --operation, distinct exit code for GraphQL errors
  • Config layering: flags > env (PDBQ_*, __ escapes underscores) > YAML
  • pdbq config example — annotated reference generated from struct tags (committed at examples/pdbq.example.yaml, can't drift)
  • pdbq config init|validate
  • pdbq plugins list with hook surfaces
  • pdbq schema print --json (introspection-format output)
  • Shell completions shipped (pdbq completion bash|zsh|fish|powershell, plus fixed-vocabulary flag value completion)

Plugin system

  • Compile-time plugins; ordered registry (priority + registration order)
  • CatalogHook — transform the introspected catalog
  • InflectionHook — override any generated name, middleware-chained
  • SchemaHook — mutate the schema IR before SDL generation
  • CompileHook — wrap SQL generation per root field
  • RequestHook — before/after operation, context + tx control
  • Enable/disable/configure per plugin via plugins.* config
  • Library embedding: pdbq.New(cfg, pdbq.WithPlugins(...))
  • Collision detection with warnings at schema build
  • Built-in: simple-names
  • Built-in: nested-mutations
  • Built-in: advanced-filters (relation filters + computed-column filtering/ordering; halves toggle via plugins.settings.advanced-filters.{relations,computed})
  • Out-of-process plugins (go-plugin gRPC or wasm) — v2 track; the existing hook interfaces are the contract either way
  • Built-in: smart-comments plugin (docs/smart-comments.md) — @omit (per-action on tables, columns, FKs, unique constraints, functions), @name, @fieldName/@foreignFieldName, @deprecated, @notNull/@nullable, @filterable/@sortable, and logical @primaryKey/@unique/@foreignKey on views; advanced-filters honors the tags per object (@omit filter,order on relations/computed functions) and gains relations_opt_in/computed_opt_in for explicit @filterable opt-in

Ops & delivery

  • Multi-stage Dockerfile → static binary on distroless, nonroot
  • Compose dev stack (Postgres + fixture + watch mode) and test stack
  • Makefile: build/dev/test/test-e2e/bench/fuzz/lint/example-config/docker-build
  • CI: vet, unit+golden with -race, e2e against Postgres 16 service, docker build
  • Golden-file compiler tests (-update regeneration)
  • E2E suite incl. RLS matrix and nested-mutation rollback atomicity
  • Fuzz: filter-input → SQL invariance (SQL text independent of values)
  • Benchmarks: schema build, compile per query shape
  • Load benchmarks (k6/vegeta) for RPS/latency against the compose stack, tracked over time (v1.x)
  • Published container image / release automation (v1.x)
  • golangci-lint config committed (Makefile falls back to go vet) (v1.x)