Skip to content

feat(server): blueprint pooling core — qualified-SQL rewrite seam, inert modules (4/6)#1333

Open
yyyyaaa wants to merge 1 commit into
feat/scale-s3-introspection-filterfrom
feat/scale-s4-pooling-core
Open

feat(server): blueprint pooling core — qualified-SQL rewrite seam, inert modules (4/6)#1333
yyyyaaa wants to merge 1 commit into
feat/scale-s3-introspection-filterfrom
feat/scale-s4-pooling-core

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

PR 4/6 of the #1325 split. The mechanism layer for sharing one PostGraphile instance per schema-shape: blueprint identity, the pooling decision, and the qualified-SQL rewrite seam. Nothing imports these modules yetgit grep confirms zero runtime references; the dispatcher wiring (and the opt-in flag becoming reachable) is PR 5. That makes this PR pure, unit-testable logic: 2,191 added lines, 1,160 of them tests.

Design constraint honored throughout: no search_path, no unqualified emission — anywhere. Instances are built fully qualified against a canonical tenant; per-request tenant routing rewrites schema identifiers in SQL text.

blueprint.ts — shared-instance identity

  • stripSchemaHashPrefix: physical <db>-<8hex>-<logical> → logical name.
  • Shape fingerprint: sha256 over sorted [logicalSchema, relname] pairs from one qualified catalog scan.
  • computeBlueprintKey: bp: + sha256({schemas, fingerprint, flags, apiName, mode, dbname}) — dbname is in the key so same-shape tenants in different physical databases never share an instance.

pooling-decision.ts — memoized per-service decision

  • Structural opt-outs (memoized until flush): realtime instances, empty schema lists, and shapes carrying bm25 indexes — see below.
  • Probe failures are transient: not memoized, so one flaky catalog query can't pin a tenant to a dedicated instance until the next flush.
  • clearPoolDecisionsForDatabase: per-database invalidation so a schema change in tenant X recomputes only X (used by PR 5's flush path).

The bm25 opt-out — answering the bm25.ts:183 review question on #1325

The direct answer: the index reference is fully qualified — no unqualified schemas, no search_path. But the review question found a real pooling gap: the bm25 adapter embeds its build-time "schema"."index" name into to_bm25query() as a bind value, and the rewrite seam maps schema identifiers in SQL text only — bind values are user data and are deliberately never rewritten. A pooled instance would therefore aim every tenant's bm25 scoring at the canonical tenant's index. Until the seam learns to rewrite regclass-shaped values, shapes carrying bm25 indexes (detected by hasBm25Indexes, one qualified pg_am probe) structurally keep per-tenant instances. Fail-safe, memoized, flush-invalidated, tested.

rewrite-pool.ts — the pool seam

  • A checkout learns its tenant from the transaction-local GUC constructive.pool_schemas, delivered through the adaptor's set_config statement. That statement is classified by exact statement text (SETTINGS_QUERY_TEXT), byte-pinned against the installed @dataplan/pg by settings-text-pin.test.ts — a dependency bump that changes the emitted text fails CI instead of silently disabling routing (drift would fail closed at runtime, but loudly-in-CI beats safely-at-runtime).
  • Rewrites double-quoted canonical schema identifiers in SQL text to the requesting tenant's schemas. Proper lexer: string literals, dollar-quoted bodies, and comments are opaque; bind values are never touched. (This is also why the seam does not use @pgsql/quotes for its emission — substituted tokens must stay double-quoted regardless of lexical safety.)
  • Prepared-statement names namespaced per tenant (sha256-prefixed) with per-connection DEALLOCATE LRU; rewrite/statement memos are 2000-entry LRUs (98.4% hit rate in the (DO NOT MERGE: superseded by stacked PRs) (WIP) proof of concept: feat(server): heap-safe instance cache + opt-in blueprint pooling (one instance per schema-shape) #1325 soak).
  • Fail-closed: a checkout with no tenant mapping refuses tenant-schema SQL rather than letting it run against the canonical tenant.
  • Composes the introspection filter (PR 3) for pooled instances; the filter-through-seam tests restored here complete PR 3's suite.

Also: ApiStructure gains optional logicalSchemas, and the metrics sampler reports rewrite/fail-closed counters.

Isolation evidence (from #1325)

13/13 scope-pollution suites clean · zero cross-tenant bleed across 104,818 authenticated requests · same-tenant SDL byte-identical (MD5) pooled vs dedicated · fail-closed counter 0 across the soak.

Testing

graphql/server 205/205 (18 suites): the 637-line rewrite-pool suite (lexer edge cases, GUC classification, fail-closed, DEALLOCATE LRU), blueprint identity + hasBm25Indexes, pooling-decision memoization/transience/bm25 opt-out, settings-text pin, full introspection-filter suite.

Stack

Stacked on #1332 (feat/scale-s3-introspection-filter); merge after it. Full stack map in #1330. Extracted from #1325 (kept open as the validated reference).

🤖 Generated with Claude Code

https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb

…ert modules)

The mechanism layer for sharing one PostGraphile instance per schema-SHAPE.
Nothing imports these modules yet — this PR is pure, unit-tested logic; the
dispatcher wiring (and the opt-in flag becoming reachable) lands in the next
PR of the stack. No search_path anywhere: instances are built fully qualified
against a canonical tenant, and per-request tenant routing happens by
rewriting schema IDENTIFIERS in SQL text.

blueprint.ts — shared-instance identity:
- stripSchemaHashPrefix: physical `<db>-<8hex>-<logical>` → logical name.
- Shape fingerprint: sha256 over sorted [logicalSchema, relname] pairs from
  one qualified catalog scan.
- computeBlueprintKey: 'bp:' + sha256({schemas, fingerprint, flags, apiName,
  mode, dbname}) — dbname included so same-shape tenants in different
  physical databases NEVER share an instance.
- hasBm25Indexes: catalog probe backing the bm25 structural opt-out below.

pooling-decision.ts — per-service decision, memoized per svc_key:
- Structural opt-outs (memoized until flush): realtime instances, empty
  schema lists, and shapes carrying bm25 indexes — the bm25 adapter embeds
  its build-time index name as a bind VALUE, which the seam deliberately
  never rewrites (answers the #1325 review question on bm25.ts; the adapter
  itself is fully qualified, but pooled routing can't reach a value).
- Probe failures are TRANSIENT: not memoized, so one flaky catalog query
  can't pin a tenant to a dedicated instance until the next flush.
- Per-database invalidation (clearPoolDecisionsForDatabase) so a schema
  change in tenant X rebuilds only X's blueprint, not the fleet.

rewrite-pool.ts — the pool seam:
- Wraps the pg Pool; a checkout learns its tenant from the transaction-local
  GUC constructive.pool_schemas (set via the adaptor's set_config statement,
  classified by EXACT statement text — SETTINGS_QUERY_TEXT, byte-pinned
  against the installed @dataplan/pg by settings-text-pin.test.ts, so a
  dependency bump fails CI instead of silently disabling routing).
- Rewrites double-quoted canonical schema identifiers in SQL text to the
  requesting tenant's schemas (lexer-based: string literals, dollar-quotes,
  comments are opaque). Bind values are never touched.
- Prepared-statement names namespaced per tenant (sha256-prefixed) with
  per-connection DEALLOCATE LRU; memoized rewrite/statement caches (2000-entry
  LRUs — 98.4% hit rate in the #1325 soak).
- FAIL-CLOSED: a checkout with no tenant mapping refuses tenant-schema SQL
  rather than letting it run against the canonical tenant.
- Composes the introspection filter (previous PR) for pooled instances.

Also: ApiStructure gains optional `logicalSchemas` (consumed by the keying),
and the metrics sampler reports rewrite/fail-closed counters.

Isolation evidence from #1325: 13/13 scope-pollution suites clean, zero
cross-tenant bleed over 104,818 authenticated requests, SDL byte-identical
(MD5) between pooled and dedicated builds of the same tenant.

Extracted from #1325 (commits 445d4c3, e38f4b5, b3bad9e, 3088062).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb
* plane schemas (e.g. `services_public`) contain no such segment and are
* returned unchanged.
*/
export const stripSchemaHashPrefix = (name: string): string => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this could be brittle, I need to understand why this is implemented

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants