feat(server): blueprint pooling core — qualified-SQL rewrite seam, inert modules (4/6)#1333
Open
yyyyaaa wants to merge 1 commit into
Open
feat(server): blueprint pooling core — qualified-SQL rewrite seam, inert modules (4/6)#1333yyyyaaa wants to merge 1 commit into
yyyyaaa wants to merge 1 commit into
Conversation
…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
pyramation
reviewed
Jul 7, 2026
| * plane schemas (e.g. `services_public`) contain no such segment and are | ||
| * returned unchanged. | ||
| */ | ||
| export const stripSchemaHashPrefix = (name: string): string => { |
Contributor
There was a problem hiding this comment.
this could be brittle, I need to understand why this is implemented
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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 yet —
git grepconfirms 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 identitystripSchemaHashPrefix: physical<db>-<8hex>-<logical>→ logical name.[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 decisionclearPoolDecisionsForDatabase: 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:183review question on #1325The 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 intoto_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 byhasBm25Indexes, one qualifiedpg_amprobe) structurally keep per-tenant instances. Fail-safe, memoized, flush-invalidated, tested.rewrite-pool.ts— the pool seamconstructive.pool_schemas, delivered through the adaptor'sset_configstatement. That statement is classified by exact statement text (SETTINGS_QUERY_TEXT), byte-pinned against the installed@dataplan/pgbysettings-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).@pgsql/quotesfor its emission — substituted tokens must stay double-quoted regardless of lexical safety.)DEALLOCATELRU; 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).Also:
ApiStructuregains optionallogicalSchemas, 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/server205/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