CONCEPT Cited by 1 source
Context-propagated SQL tags¶
Context-propagated SQL tags is the idiom for attaching
per-request metadata to SQL queries in languages without an
implicit request-local storage convention — most canonically Go,
which uses an explicit context.Context threaded
through every function signature as the storage vehicle.
The problem it solves¶
SQLCommenter tags must
reach every SELECT / INSERT / UPDATE / DELETE the service
issues during a request. Rails, Django, Spring, and Node.js all
offer ORM-layer or middleware-layer hooks that capture request
context at a single central point and emit tags on every query
automatically
(ORM-layer
tag propagation). Go has no equivalent framework-absorbed
request-local storage — net/http passes the request via an
explicit *http.Request argument, and goroutines do not inherit
parent request-scoped state implicitly.
The Go-idiomatic answer is to carry the tag set on
context.Context, rely on the convention that every database-
touching function signature accepts a ctx context.Context, and
render the tags to SQL at the last possible moment — when the
wrapped QueryContext / ExecContext method builds the final
tagged query string.
Shape¶
Three primitives compose to make the pattern work:
-
A typed context key +
tagsFromContext(ctx)reader that returns a copy of the tag map (so call sites can't mutate shared state):type contextKey string const sqlTagsKey contextKey = "sql_tags" func tagsFromContext(ctx context.Context) map[string]string { if tags, ok := ctx.Value(sqlTagsKey).(map[string]string); ok { out := make(map[string]string, len(tags)) for k, v := range tags { out[k] = v } return out } return make(map[string]string) } func contextWithTags(ctx context.Context, tags map[string]string) context.Context { return context.WithValue(ctx, sqlTagsKey, tags) } -
Middlewares that inject keys at the layer where the information is available — HTTP middleware for
route, auth middleware fortier, startup env-var read forfeature/deployment, etc. Each layer writes back a new context with its key added. -
A wrapper
QueryContext/ExecContexton the database handle that pulls the map out and renders tags to SQL:
Why copy-on-read¶
Go maps are reference types with no built-in protection against
concurrent writes. If tagsFromContext returned the same map,
a middleware that set tags["tier"] = ... could racily mutate
a map that another goroutine was iterating over for tag-
rendering. Copy-on-read makes the read semantics match
context.WithValue's immutable intent at modest allocation
cost.
Canonical verbatim framing (Source: sources/2026-04-21-planetscale-patterns-for-postgres-traffic-control): "return a copy so callers can't mutate shared state."
Comparison with ORM-middleware pattern¶
| Axis | ORM-middleware (Rails / Django / Spring) | Context-propagated (Go) |
|---|---|---|
| Storage | Thread-local / request-local / ORM hook | context.Context via explicit arg |
| Attach time | One central middleware install | Per-layer context enrichment |
| Render time | ORM emits on every query automatically | Wrapper QueryContext renders on each call |
| Coverage | Anything the ORM issues (libraries too) | Anything that accepts ctx (convention) |
| Discipline cost | Install middleware once | Every function signature carries ctx |
| Library queries | Captured automatically | Captured iff library accepts ctx |
The Go variant is more explicit (every signature exposes
the propagation) and slightly narrower (pre-ctx libraries
don't pick up the tags) but identical in on-the-wire format
— both emit SQLCommenter-format SQL comments.
Generalises beyond Go¶
The pattern applies to any language where request-local storage is explicit rather than implicit:
- Rust via
http::Extensionsonhyper::Request/ actixExtensions/ axum's typed extractors — tags live in a typed extension slot, a wrapper method renders. - Python's
contextvars(PEP 567) — async-safe request- local storage;asyncpgorpsycopg3wrappers can render from it. - C++ / systems languages with coroutine-local storage primitives.
The common shape: a per-request typed storage mechanism + a database-wrapper method that reads from it + middlewares that write into it at each enrichment layer.
Operational properties¶
- Deterministic tag ordering is mandatory. Two calls with
the same logical tag set must produce the same SQL text to
keep Postgres plan-cache hits stable.
sort.Strings(parts)on the emitted key-value strings is the standard fix (canonicalised in the Brown post). - URL-encoding is non-negotiable for values.
route='/api/ export'contains/which is fine in SQL comments but reserved for some observability-tool parsers;url.QueryEscapeis the canonical choice. Downstream tools must decode. - Per-query rendering cost is O(N·logN) for N tags due to
sort.Strings. For single-digit N this is negligible, but under ultra-high QPS the cost is real and a memoisation layer may be justified. - Middleware-ordering matters.
routemiddleware must run after the auth middleware that setstier, so the tags map already exists when the route middleware reads- modifies-writes it. Most production code stacks middlewares outside-in from request entry.
Seen in¶
- sources/2026-04-21-planetscale-patterns-for-postgres-traffic-control
— canonical wiki introduction. Josh Brown canonicalises
the two-helper substrate (
appendTags+tagsFromContext+contextWithTags) and five middleware / initialisation points that write into the tag map: HTTP middleware (route), auth middleware (tier), startup env-var read (deployment), feature-flag evaluation (feature), dedicated connection pool (application_name). Same five axes compose on onecontext.Context, rendering to one tagged SQL string.