Skip to content

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:

  1. 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)
    }
    

  2. Middlewares that inject keys at the layer where the information is available — HTTP middleware for route, auth middleware for tier, startup env-var read for feature / deployment, etc. Each layer writes back a new context with its key added.

  3. A wrapper QueryContext / ExecContext on the database handle that pulls the map out and renders tags to SQL:

    func (db *DB) QueryContext(ctx context.Context, query string, args ...any) (*sql.Rows, error) {
        return db.sql.QueryContext(ctx, appendTags(query, tagsFromContext(ctx)), args...)
    }
    

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::Extensions on hyper::Request / actix Extensions / 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; asyncpg or psycopg3 wrappers 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.QueryEscape is 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. route middleware must run after the auth middleware that sets tier, 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-controlcanonical 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 one context.Context, rendering to one tagged SQL string.
Last updated · 378 distilled / 1,213 read