Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
165 commits
Select commit Hold shift + click to select a range
1e1f2c0
feat(sqlite): wire function registry and array operator support for S…
tamnd Jun 10, 2026
ec5369d
fix(sqlite): FK enforcement, bool/JSON coercions in drain, cast colum…
tamnd Jun 10, 2026
016eba6
fix(sqlite): guard json.RawMessage coercion with json.Valid
tamnd Jun 10, 2026
f869420
fix(ci): gofmt, array-op type guard, planner column-type enrichment
tamnd Jun 10, 2026
229393d
feat(mysql): JSON array containment via JSON_CONTAINS/JSON_OVERLAPS
tamnd Jun 10, 2026
100a9c1
feat(sqlserver): native RPC via EXEC and compileNativeCall
tamnd Jun 10, 2026
f852bc5
fix(sqlserver): five compat fixes for SQL Server 2022
tamnd Jun 11, 2026
77ab670
sqlserver: fix ArrayLiteral PG-quoting and rewrite upsert as MERGE
tamnd Jun 11, 2026
5f57b75
sqlserver: fix error mapping, IDENTITY_INSERT for upsert, identity in…
tamnd Jun 11, 2026
6fe23c0
sqlserver: only set IDENTITY_INSERT ON when identity col is in payload
tamnd Jun 11, 2026
b4d2cf3
mysql: fix IsBool, datetime args, and UpdateReturn
tamnd Jun 11, 2026
ed9bb73
mysql: fix UpdateReturn by pre-capturing PKs before UPDATE
tamnd Jun 11, 2026
c3d1263
pgerr: add PGRST102 and move Content-Type errors onto it
tamnd Jun 11, 2026
65622bc
pgerr: rename the internal error code to PGRSTX00
tamnd Jun 11, 2026
6ce97e2
config: accept the full v14 key set
tamnd Jun 11, 2026
551c086
pgerr: adopt the v14 message texts for PGRST116, PGRST127, and 22P02
tamnd Jun 11, 2026
1fc777c
pgerr: let the error envelope carry non-string details
tamnd Jun 11, 2026
db63d95
pgerr: add 42703 for unknown columns outside the schema cache
tamnd Jun 11, 2026
8e99ae3
pgerr: add upstream texts for wrong-verb RPC and 25006
tamnd Jun 11, 2026
3fd751c
pgerr: spell the 42501 status rule in one place
tamnd Jun 11, 2026
cd89851
httpapi: serve CORS the way PostgREST does
tamnd Jun 11, 2026
978bca7
pgerr: add PGRST111, PGRST112, PGRST125, and PGRST203 constructors
tamnd Jun 11, 2026
7e1c97d
httpapi: enforce db-max-rows on reads and RPC
tamnd Jun 11, 2026
478c51e
auth: align JWT errors with the v14 code split and WWW-Authenticate
tamnd Jun 11, 2026
a245211
pgerr: assemble RAISE SQLSTATE 'PGRST' errors and add PGRST121
tamnd Jun 11, 2026
6a75ee0
pgerr: pass constraint-violation text through verbatim
tamnd Jun 11, 2026
13c1bfa
pgerr: name the error code in Proxy-Status
tamnd Jun 11, 2026
0b63464
httpapi: resolve the active schema from the profile headers
tamnd Jun 11, 2026
2bc46ce
auth: fail closed when jwt-secret or the anon role is unset
tamnd Jun 11, 2026
69bf104
adminapi: add the admin server
tamnd Jun 11, 2026
5782344
httpapi: negotiate the root Accept header and add the charset
tamnd Jun 11, 2026
dfd516d
auth: parse jwt-role-claim-key as the JSPath DSL
tamnd Jun 11, 2026
c312772
cmd: reload on SIGUSR1 and SIGUSR2
tamnd Jun 11, 2026
4f6cc5e
openapi: filter the document by role privileges in follow-privileges …
tamnd Jun 11, 2026
8f9efac
auth: accept JWK and JWK Set key material in jwt-secret
tamnd Jun 11, 2026
5580130
cmd: grow the PostgREST CLI surface
tamnd Jun 11, 2026
42a7f94
config: expand $(VAR) and @file values
tamnd Jun 11, 2026
a9b0f98
auth: validate claims the PostgREST way
tamnd Jun 11, 2026
68c7c51
config: support the server-host special values
tamnd Jun 11, 2026
55fc971
config: give db-schemas an engine-aware default
tamnd Jun 11, 2026
1a91f35
openapi: make openapi-security-active configurable and match the v14 …
tamnd Jun 11, 2026
882b0ff
config: take pool timeouts as plain seconds and use them
tamnd Jun 12, 2026
c584594
httpapi: carry db-pre-request to the backend, refuse it elsewhere
tamnd Jun 12, 2026
cce9eb3
httpapi: gate execution plans on db-plan-enabled
tamnd Jun 12, 2026
482d9da
cmd: serve the API over server-unix-socket
tamnd Jun 12, 2026
d66ce88
cmd: filter the request log by log-level
tamnd Jun 12, 2026
60ed7d6
auth: stop downgrading bad credentials to anon
tamnd Jun 12, 2026
89ec7c0
httpapi: answer root verbs the way v14 does
tamnd Jun 12, 2026
2f96cbc
httpapi: carry the transaction settings to the backend
tamnd Jun 12, 2026
bc3afcf
config: let the postgres backend run without db-uri
tamnd Jun 12, 2026
92a5ba7
config: warn when every request would be anonymous
tamnd Jun 12, 2026
0882ccb
config: keep dbrest extensions out of the PGRST namespace
tamnd Jun 12, 2026
4e8f5b1
authz: parse the policy registry and wire it into the binary
tamnd Jun 12, 2026
0dd9f04
authz: reject star projection under a column-limited grant
tamnd Jun 12, 2026
176f5b5
openapi: match the v14 document shape
tamnd Jun 12, 2026
522176a
cmd: decode jwt-secret when jwt-secret-is-base64 is set
tamnd Jun 12, 2026
478daab
openapi: pipe database comments into the document
tamnd Jun 12, 2026
eb379c0
httpapi: embed declared-json rpc scalars verbatim
tamnd Jun 12, 2026
dcb7772
httpapi: let db-root-spec replace the document
tamnd Jun 12, 2026
085d548
schema: publish the model through an atomic cache
tamnd Jun 12, 2026
16f17a7
Merge branch 'fix/v14-auth' into fix/v14-compat
tamnd Jun 12, 2026
b2c80e0
Merge branch 'fix/v14-config' into fix/v14-compat
tamnd Jun 12, 2026
ead4255
Merge branch 'fix/v14-openapi' into fix/v14-compat
tamnd Jun 12, 2026
a77e331
sqlgen: route payload arrays through the dialect on writes
tamnd Jun 12, 2026
6f08491
rpc: bind reserved request-context placeholders on the emulated path
tamnd Jun 13, 2026
f6a7938
sqlite: report upsert 200 vs 201 by detecting pre-existing keys
tamnd Jun 13, 2026
bd5949e
compat: make write cases self-cleaning and seed an anon role
tamnd Jun 13, 2026
8574e06
writes: enforce the PUT contract, fix upsert promotion and missing de…
tamnd Jun 13, 2026
c789dc6
writes: shape the representation by the select projection and resolve…
tamnd Jun 13, 2026
c56ecac
writes: match PostgREST CSV semantics and reject ragged bulk inserts
tamnd Jun 13, 2026
ff5a59d
Filter parents on embedded-resource existence
tamnd Jun 13, 2026
16b2424
Tighten filter parsing toward v14 grammar
tamnd Jun 13, 2026
57e9b97
Support aggregate select syntax behind db-aggregates-enabled
tamnd Jun 13, 2026
ae07afc
Parse composed embed hints alongside !inner
tamnd Jun 13, 2026
31ec2b0
Detect one-to-one and junction edges by key constraints
tamnd Jun 13, 2026
cbb467b
Add a declared and computed relationships registry
tamnd Jun 13, 2026
d918892
Embed through views by projecting base-table foreign keys
tamnd Jun 13, 2026
fe069e6
rpc: distinguish ambiguous overloads from no-match in call resolution
tamnd Jun 13, 2026
71c7549
rpc: model a void return as 200 with a null body
tamnd Jun 13, 2026
1a78f09
rpc: split GET arguments from filters once the signature is known
tamnd Jun 13, 2026
1e63474
rpc: lower variadic parameters across both verbs
tamnd Jun 13, 2026
5398875
rpc: bind the whole body for a single-unnamed-parameter function
tamnd Jun 13, 2026
419909d
httpapi: answer OPTIONS with Allow and raise PGRST117 for unknown verbs
tamnd Jun 13, 2026
9bbbf76
ir: enforce handling=strict and fix Prefer dedup, lenient, ordering
tamnd Jun 13, 2026
24c4ee1
httpapi: accept the suffixless vnd.pgrst vendor media types
tamnd Jun 13, 2026
eb5ca22
httpapi: return 206 for any partial count and treat offset==total as …
tamnd Jun 13, 2026
df5218c
Distinguish range errors from parse errors on reads
tamnd Jun 13, 2026
983303e
Drop the fixed 16 MiB body cap for an opt-in limit
tamnd Jun 13, 2026
d42d75f
Emit Server-Timing when server-timing-enabled is set
tamnd Jun 13, 2026
cdb49ad
Gate Prefer: tx= on the db-tx-end policy
tamnd Jun 13, 2026
9f57684
Route RPC reads through the shared pagination path
tamnd Jun 13, 2026
420777f
Shape write Content-Range per method and fix upsert/PUT status
tamnd Jun 13, 2026
948bba4
Serialize JSON object keys in select order
tamnd Jun 13, 2026
61de2f1
Honor the nulls=stripped media type parameter
tamnd Jun 13, 2026
b7f9f3f
Render CSV booleans as t/f, pin quoting and empty-result shape
tamnd Jun 13, 2026
34f7fe9
Enforce Prefer: max-affected on writes with PGRST124
tamnd Jun 13, 2026
cbe72e9
Cover the full execution-plan media type
tamnd Jun 13, 2026
337d033
Honor Prefer: timezone= on reads, writes, and RPC
tamnd Jun 13, 2026
9b4890b
Base native 42501 at 403 so authenticated denials are forbidden
tamnd Jun 13, 2026
eb077cb
Run db-pre-request on postgres instead of refusing it everywhere
tamnd Jun 13, 2026
016eabf
Compile is.unknown and coerce eq.true/false by column type
tamnd Jun 13, 2026
19945df
Hide empty-parenthesis embeds from the projection
tamnd Jun 13, 2026
6b06d5d
Compile JSON arrow paths in select, filter, and order
tamnd Jun 13, 2026
68c6592
Shape write representation by order, limit and offset
tamnd Jun 13, 2026
5209388
Accept empty write payloads as a no-op instead of 400
tamnd Jun 13, 2026
9aacb58
Lower range operators sl/sr/nxr/nxl/adj through a dialect hook
tamnd Jun 13, 2026
f117877
Support top-level ordering by an embedded to-one column
tamnd Jun 13, 2026
5eece01
Apply !inner embed EXISTS to the exact count
tamnd Jun 13, 2026
7504198
Estimate planned and estimated counts on postgres
tamnd Jun 13, 2026
e65d12a
Cover empty-parenthesis embeds that hide their key
tamnd Jun 13, 2026
3f33823
Lower spread embeds to lifted parent columns
tamnd Jun 13, 2026
27136f7
Support embeds on RPC results over a relation return
tamnd Jun 13, 2026
46073ea
Enforce singular write cardinality before commit
tamnd Jun 13, 2026
afe80c0
Build native RPC counts in the backend, not the registry compiler
tamnd Jun 13, 2026
dbbd8af
Let portable RPC functions steer the response via reserved columns
tamnd Jun 13, 2026
21dee6b
Validate function response controls, emit PGRST111/PGRST112
tamnd Jun 13, 2026
69cf7ec
Forward constraint errors faithfully instead of canonicalizing
tamnd Jun 13, 2026
338fa48
Return 42703 for unknown select/filter/order columns
tamnd Jun 13, 2026
e56a03a
Render PGRST200/201 details and hint payloads
tamnd Jun 13, 2026
12cc503
Schema-qualify the PGRST205 relation name
tamnd Jun 13, 2026
fe38b03
Frame PGRST127 as upstream's code and record the PGRST100 message div…
tamnd Jun 13, 2026
e6471e0
Surface RAISE SQLSTATE 'PGRST' full-control errors from the postgres …
tamnd Jun 13, 2026
2b8c333
Align body, RPC, path, and privilege errors with v14 codes
tamnd Jun 13, 2026
e055006
Make the sqlite like operator case-sensitive
tamnd Jun 13, 2026
d6942d9
Build the insert test value as JSON, not raw text
tamnd Jun 13, 2026
c120909
Apply read clauses to the native RPC path
tamnd Jun 13, 2026
74958f8
Dispatch native RPC to the negotiated schema
tamnd Jun 13, 2026
5fd9b43
Splice native RPC JSON args as untyped literals
tamnd Jun 13, 2026
42d55e6
Render temporal columns as PostgreSQL's own JSON spellings
tamnd Jun 13, 2026
f4e8f4e
Pass validated cast targets through to PostgreSQL
tamnd Jun 13, 2026
1702d2a
Validate aggregate output cast targets too
tamnd Jun 13, 2026
6308165
Match tsvector columns directly in full-text search
tamnd Jun 13, 2026
948e6b3
Route a JSON array payload by its target column type
tamnd Jun 13, 2026
a1ec167
Chunk a wide embed's JSON object past the argument cap
tamnd Jun 13, 2026
641e821
Pin a counted read to one snapshot on postgres
tamnd Jun 13, 2026
f4d34cb
Degrade a no-target upsert to a plain insert
tamnd Jun 13, 2026
e0ca163
Lower an in-list to = ANY on postgres
tamnd Jun 13, 2026
48068dd
Match PostgREST's request context GUC shapes
tamnd Jun 13, 2026
41598fc
postgres: build search_path per request from the active schema
tamnd Jun 13, 2026
0c07b48
postgres: native RPC access mode follows function volatility
tamnd Jun 13, 2026
d3a3689
postgres: replay impersonated role settings per request
tamnd Jun 13, 2026
8d4905c
postgres: read response GUCs on read paths
tamnd Jun 13, 2026
09355d5
postgres: honor a DSN-chosen query exec mode
tamnd Jun 13, 2026
559894b
postgres: hoist function SET options to the transaction
tamnd Jun 13, 2026
964d200
postgres: expose matviews and foreign tables, drop partition leaves
tamnd Jun 13, 2026
3742ac3
postgres: introspect unique sets, identity columns, and comments
tamnd Jun 13, 2026
f704a42
postgres: confirm void RPC answers 204 on the read path
tamnd Jun 13, 2026
ce417ec
postgres: render range and multirange columns as PostgreSQL text
tamnd Jun 13, 2026
367cbad
postgres: match PostgREST status table edges and connection-error family
tamnd Jun 13, 2026
5a2fbb2
postgres: shape native RPC results from pg_proc, not column names
tamnd Jun 13, 2026
b8ab0bc
Count volatile RPC results in a single execution
tamnd Jun 13, 2026
e820792
Introspect function signatures into a native registry
tamnd Jun 13, 2026
eb80462
Route native RPC through the shared planner
tamnd Jun 13, 2026
52cd4d0
Emit native RPC functions in the OpenAPI document
tamnd Jun 13, 2026
ecf97a9
Merge the portable and native registries on postgres
tamnd Jun 13, 2026
44560e8
Infer view foreign keys from base tables on postgres
tamnd Jun 13, 2026
710a6bb
Introspect computed fields as virtual columns on postgres
tamnd Jun 13, 2026
f7dec84
Introspect computed relationships as embeddable edges on postgres
tamnd Jun 13, 2026
fefaf55
Add data representations over domain casts
tamnd Jun 13, 2026
4aeff96
Wire postgres into the conformance harness
tamnd Jun 13, 2026
1fcfa1b
Apply data representations to match, in, and array filters
tamnd Jun 13, 2026
85c8fc8
Annotate only single-column foreign keys in OpenAPI
tamnd Jun 13, 2026
65f7936
Wire db-prepared-statements through to the postgres exec mode
tamnd Jun 13, 2026
5484cdd
Reload over the db-channel via LISTEN/NOTIFY
tamnd Jun 13, 2026
959df82
Clear the golangci-lint backlog so CI goes green
tamnd Jun 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions adminapi/adminapi.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
// Package adminapi is the admin listener PostgREST runs next to the API when
// admin-server-port is set: GET /live and /ready for orchestrator probes,
// GET /schema_cache for the loaded cache, and GET /metrics in Prometheus text
// format. The endpoints, paths, and status codes mirror PostgREST v14's
// admin server (PostgREST.Admin): /live is 200 while the API listener accepts
// connections and 500 otherwise; /ready adds the backend and schema cache
// health and degrades to 503; any other path is 404 with an empty body.
//
// Spec 20 sketches a POST /schema_cache for an on-demand reload; upstream has
// no such endpoint (reload is SIGUSR1 or NOTIFY), so it is not served here.
// If the reload entry point lands it belongs next to the signal handler, not
// in this package's GET-only surface.
package adminapi

import (
"context"
"fmt"
"net/http"
"sort"
"strings"
"sync"
"time"
)

// Server serves the admin endpoints. The health checks are injected by the
// command, which knows the API listener address and owns the backend; the
// zero value of any field degrades gracefully (a nil check reports healthy,
// a nil SchemaCache serves an empty body, matching upstream's "no cache yet").
type Server struct {
// Live reports whether the API listener accepts connections. PostgREST
// implements this as a TCP dial of its own socket; the command wires the
// same here.
Live func(ctx context.Context) error

// Ready reports whether the backend connection and the schema cache are
// usable. It is consulted in addition to Live.
Ready func(ctx context.Context) error

// SchemaCache returns the loaded schema cache rendered as JSON.
SchemaCache func() ([]byte, error)

// Metrics holds the counters rendered at /metrics.
Metrics *Metrics
}

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
switch strings.TrimSuffix(r.URL.Path, "/") {
case "/live":
w.WriteHeader(s.liveStatus(r.Context()))
case "/ready":
w.WriteHeader(s.readyStatus(r.Context()))
case "/schema_cache":
body, err := s.schemaCacheJSON()
if err != nil {
w.WriteHeader(http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(body)
case "/metrics":
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
if s.Metrics != nil {
w.Write([]byte(s.Metrics.Text()))
}
default:
w.WriteHeader(http.StatusNotFound)
}
}

func (s *Server) liveStatus(ctx context.Context) int {
if s.Live != nil && s.Live(ctx) != nil {
return http.StatusInternalServerError
}
return http.StatusOK
}

func (s *Server) readyStatus(ctx context.Context) int {
if status := s.liveStatus(ctx); status != http.StatusOK {
return status
}
if s.Ready != nil && s.Ready(ctx) != nil {
return http.StatusServiceUnavailable
}
return http.StatusOK
}

func (s *Server) schemaCacheJSON() ([]byte, error) {
if s.SchemaCache == nil {
return nil, nil
}
return s.SchemaCache()
}

// Metrics is a small Prometheus-text registry covering what dbrest measures
// today: schema cache loads and the configured pool ceiling. The names follow
// PostgREST's metric names where the concept matches.
type Metrics struct {
mu sync.Mutex
loads map[string]int64 // by status label: SUCCESS / FAIL
lastLoadSeconds float64
poolMax int
}

// NewMetrics builds a registry; poolMax is the db-pool setting.
func NewMetrics(poolMax int) *Metrics {
return &Metrics{loads: map[string]int64{}, poolMax: poolMax}
}

// ObserveSchemaCacheLoad records one schema cache load attempt.
func (m *Metrics) ObserveSchemaCacheLoad(d time.Duration, err error) {
m.mu.Lock()
defer m.mu.Unlock()
status := "SUCCESS"
if err != nil {
status = "FAIL"
}
m.loads[status]++
if err == nil {
m.lastLoadSeconds = d.Seconds()
}
}

// Text renders the registry in the Prometheus text exposition format.
func (m *Metrics) Text() string {
m.mu.Lock()
defer m.mu.Unlock()
var b strings.Builder
b.WriteString("# HELP pgrst_schema_cache_query_time_seconds The query time in seconds of the last schema cache load\n")
b.WriteString("# TYPE pgrst_schema_cache_query_time_seconds gauge\n")
fmt.Fprintf(&b, "pgrst_schema_cache_query_time_seconds %g\n", m.lastLoadSeconds)
b.WriteString("# HELP pgrst_schema_cache_loads_total The total number of schema cache loads\n")
b.WriteString("# TYPE pgrst_schema_cache_loads_total counter\n")
statuses := make([]string, 0, len(m.loads))
for status := range m.loads {
statuses = append(statuses, status)
}
sort.Strings(statuses)
for _, status := range statuses {
fmt.Fprintf(&b, "pgrst_schema_cache_loads_total{status=%q} %d\n", status, m.loads[status])
}
b.WriteString("# HELP pgrst_db_pool_max Max pool connections\n")
b.WriteString("# TYPE pgrst_db_pool_max gauge\n")
fmt.Fprintf(&b, "pgrst_db_pool_max %d\n", m.poolMax)
return b.String()
}
132 changes: 132 additions & 0 deletions adminapi/adminapi_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package adminapi

import (
"context"
"encoding/json"
"errors"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)

func get(t *testing.T, s *Server, path string) *http.Response {
t.Helper()
rec := httptest.NewRecorder()
s.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, path, nil))
return rec.Result()
}

// TestLive covers both sides of the liveness probe: 200 while the API socket
// answers, 500 once it does not, matching the PostgREST admin server.
func TestLive(t *testing.T) {
up := &Server{Live: func(context.Context) error { return nil }}
if resp := get(t, up, "/live"); resp.StatusCode != http.StatusOK {
t.Errorf("live up: status = %d, want 200", resp.StatusCode)
}
down := &Server{Live: func(context.Context) error { return errors.New("refused") }}
if resp := get(t, down, "/live"); resp.StatusCode != http.StatusInternalServerError {
t.Errorf("live down: status = %d, want 500", resp.StatusCode)
}
}

// TestReady covers the three readiness answers: 500 when the API is not
// reachable, 503 when it is up but the backend is not usable, 200 otherwise.
func TestReady(t *testing.T) {
ok := func(context.Context) error { return nil }
bad := func(context.Context) error { return errors.New("down") }

cases := []struct {
name string
srv *Server
want int
}{
{"loaded", &Server{Live: ok, Ready: ok}, http.StatusOK},
{"backend pending", &Server{Live: ok, Ready: bad}, http.StatusServiceUnavailable},
{"api unreachable", &Server{Live: bad, Ready: ok}, http.StatusInternalServerError},
}
for _, tc := range cases {
if resp := get(t, tc.srv, "/ready"); resp.StatusCode != tc.want {
t.Errorf("%s: status = %d, want %d", tc.name, resp.StatusCode, tc.want)
}
}
}

// TestSchemaCache checks the dump is served as JSON, and that a failing dump
// degrades to 500 rather than half a body.
func TestSchemaCache(t *testing.T) {
srv := &Server{SchemaCache: func() ([]byte, error) {
return json.Marshal(map[string]any{"relations": []string{"films"}})
}}
resp := get(t, srv, "/schema_cache")
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "application/json") {
t.Errorf("Content-Type = %q, want application/json", ct)
}
var body map[string]any
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
t.Fatalf("decode: %v", err)
}

broken := &Server{SchemaCache: func() ([]byte, error) { return nil, errors.New("nope") }}
if resp := get(t, broken, "/schema_cache"); resp.StatusCode != http.StatusInternalServerError {
t.Errorf("broken dump: status = %d, want 500", resp.StatusCode)
}
}

// TestMetrics checks the Prometheus text rendering: content type, the load
// counters by status, the last query time, and the pool gauge.
func TestMetrics(t *testing.T) {
m := NewMetrics(10)
m.ObserveSchemaCacheLoad(250*time.Millisecond, nil)
m.ObserveSchemaCacheLoad(0, errors.New("introspect failed"))
srv := &Server{Metrics: m}

resp := get(t, srv, "/metrics")
if resp.StatusCode != http.StatusOK {
t.Fatalf("status = %d, want 200", resp.StatusCode)
}
if ct := resp.Header.Get("Content-Type"); !strings.HasPrefix(ct, "text/plain") {
t.Errorf("Content-Type = %q, want text/plain", ct)
}
raw, err := io.ReadAll(resp.Body)
if err != nil {
t.Fatalf("read body: %v", err)
}
body := string(raw)
for _, want := range []string{
`pgrst_schema_cache_loads_total{status="SUCCESS"} 1`,
`pgrst_schema_cache_loads_total{status="FAIL"} 1`,
"pgrst_schema_cache_query_time_seconds 0.25",
"pgrst_db_pool_max 10",
} {
if !strings.Contains(body, want) {
t.Errorf("metrics body missing %q\n%s", want, body)
}
}
}

// TestUnknownPathIs404 checks the fall-through, including the root.
func TestUnknownPathIs404(t *testing.T) {
srv := &Server{}
for _, path := range []string{"/", "/config", "/live/extra"} {
if resp := get(t, srv, path); resp.StatusCode != http.StatusNotFound {
t.Errorf("%s: status = %d, want 404", path, resp.StatusCode)
}
}
}

// TestNilChecksDegradeGracefully checks the zero-value server: health reports
// up (nothing to check), the cache dump is empty, metrics body is empty.
func TestNilChecksDegradeGracefully(t *testing.T) {
srv := &Server{}
for _, path := range []string{"/live", "/ready", "/schema_cache", "/metrics"} {
if resp := get(t, srv, path); resp.StatusCode != http.StatusOK {
t.Errorf("%s: status = %d, want 200", path, resp.StatusCode)
}
}
}
Loading
Loading