Skip to content

fix(graphile): tenant-isolation fixes for i18n/llm/search/meta-schema + @pgsql/quotes adoption (1/6)#1330

Open
yyyyaaa wants to merge 4 commits into
mainfrom
feat/scale-s1-plugin-fixes
Open

fix(graphile): tenant-isolation fixes for i18n/llm/search/meta-schema + @pgsql/quotes adoption (1/6)#1330
yyyyaaa wants to merge 4 commits into
mainfrom
feat/scale-s1-plugin-fixes

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

First PR of the 6-PR stack that splits #1325 into incrementally mergeable pieces (per review feedback there). This one carries the four standalone correctness/tenant-isolation bugs the #1325 audit found in plugins — every one of them is live behavior today and none depends on caching or pooling — plus the requested @pgsql/quotes adoption.

Fixes

  • graphile-i18n — RLS bypass. The localeStrings translations query ran through withPgClient(null, …): no role, no JWT claims, so RLS on the base + translation tables was not applied for that read. It now passes the request's pgSettings. Also: the locale GraphQLObjectType cache is reset per build (init hook) so a rebuilt schema can't reuse type objects from a previous build.
  • graphile-llm (agent-discovery) — cross-database config leak. The discovery query had no database filter: LIMIT 1 over a control-plane table holding every database's rows, so a process serving multiple databases could return another database's agent config. Now filtered by the requester's jwt.claims.database_id, failing closed when absent, with the 60s cache keyed by database id.
  • graphile-settings (meta-schema) — concurrent-build _meta bleed. Collected table metadata lived in a module-global; concurrent schema builds interleave init hooks, so one build's _meta could serve another build's tables. Now keyed by the build object (WeakMap). The flat mirror stays for out-of-process codegen consumers (single-build, last-write-wins is safe there).
  • graphile-llm (rag) / graphile-search (bm25) — qualification hygiene. Chunk-table and bm25-index references stay schema-qualified; SQL builders exported for unit testing.

@pgsql/quotes adoption

Addresses the review comment on graphile-i18n/src/plugin.ts in #1325. Hand-rolled "${schema}"."${table}" templates in i18n, rag, and bm25 are replaced with QuoteUtils.quoteIdentifier / quoteQualifiedIdentifier.

Two properties worth knowing:

  • The library ports PostgreSQL's quote_ident: identifiers come out bare when lexically safe and double-quoted (with " doubled) when required — strictly more correct than always-quote templates for case-sensitive names, reserved keywords, and embedded quotes. Test assertions were updated for the bare snake_case forms.
  • Load-bearing and now pinned by tests: hashed multi-tenant schema names contain -, so they are always emitted double-quoted. The pooling rewrite seam (later in this stack) matches double-quoted schema tokens in SQL text, so tenant-schema references in plugin SQL must never be emitted bare. New test cases assert the dashed-schema quoting in both rag and bm25.

Where it is deliberately not used: the rewrite seam itself (PR 4 of the stack) must always emit quoted identifiers when substituting tokens, so quote_ident-style bare output is wrong there by construction.

@pgsql/quotes@17.1.0 is already in the workspace dependency graph (graphile-settings, presigned-url, realtime, pgpm/cli) — zero new install weight; the three plugin package.jsons just declare it.

Commits

  1. The four fixes (verbatim from (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).
  2. chore(lockfile) — pnpm 10.30 re-normalization with no dependency changes, split out so the real deltas stay readable (~10k mechanical lines; the same churn is embedded in (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's lockfile diff).
  3. The quotes adoption (+137/−31 with a 44-line lockfile delta).

Testing

graphile-i18n 5/5 · graphile-llm 58/58 non-Ollama (11 env-dependent Ollama integration tests need the CI service container) incl. 11 new agent-discovery/rag tests · graphile-search 78/78 incl. new quoting tests · graphile-settings 158/158.

Stack

# branch contents
1 (this) #1330 feat/scale-s1-plugin-fixes plugin correctness fixes + @pgsql/quotes
2 #1331 feat/scale-s2-cache-hardening heap-safe cache, memory governor, build admission
3 #1332 feat/scale-s3-introspection-filter namespace-scoped introspection (87× per-instance memory)
4 #1333 feat/scale-s4-pooling-core blueprint pooling core — rewrite seam (inert modules)
5 #1334 feat/scale-s5-pooling-integration opt-in pooling integration (GRAPHILE_BLUEPRINT_POOLING)
6 #1335 feat/scale-s6-perf-tooling cperf harness + validation rig + capacity docs

Each PR targets the previous branch; merge top-to-bottom (GitHub retargets children automatically when a base merges). Extracted from #1325, which stays open as the validated end-to-end reference — all numbers cited in this stack were measured there on a production-shaped rig (48 tenant DBs, 61k pg_class rows).

🤖 Generated with Claude Code

https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb

yyyyaaa and others added 3 commits July 6, 2026 17:18
…search, meta-schema

Four defects found while auditing plugin behavior on shared/multi-schema
processes; every fix is live behavior today (none depends on any caching or
pooling change):

- graphile-i18n: run the translations query through withPgClient with the
  REQUEST's pgSettings instead of null. With null the query ran outside the
  requester's role/claims — bypassing RLS on base + translation tables. Also
  reset the locale GraphQLObjectType cache per build (init hook) so a rebuilt
  schema never reuses type objects from a previous build.

- graphile-llm (agent-discovery): the discovery query had no database filter —
  `LIMIT 1` over a control-plane table that holds EVERY database's rows, so a
  process serving multiple databases could return another database's agent
  config. Filter by the requester's jwt.claims.database_id, fail closed when
  absent, and key the 60s cache by database id (with a :nodb spillover key)
  instead of dbname.

- graphile-llm (rag): keep chunk-table references schema-qualified and export
  the SQL builders for unit testing.

- graphile-search (bm25): pass the schema-qualified index name to
  to_bm25query; document the store-lookup keying accurately.

- graphile-settings (meta-schema): key collected table metadata by the build
  object (WeakMap) instead of a module-global array. Concurrent schema builds
  in one process interleave init hooks, so the global let one build's _meta
  serve another build's tables. The flat mirror stays for out-of-process
  codegen consumers (single-build, last-write-wins is safe there).

Extracted from #1325 (commits d320a27, f62fb69, 0d1496e, b3bad9e).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb
…ndency changes)

`pnpm install --lockfile-only` with UNCHANGED manifests rewrites the lockfile
into pnpm 10.30's canonical form (mostly dedupe). Committed separately so the
dependency changes in this stack stay readable; identical normalization churn
is embedded in #1325's lockfile diff.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb
…rag, bm25

Replace hand-rolled `"${schema}"."${table}"` templates with
QuoteUtils.quoteIdentifier / quoteQualifiedIdentifier (requested in #1325
review). The library ports PostgreSQL's quote_ident: identifiers are emitted
bare when lexically safe and double-quoted (with embedded-quote doubling) when
required — strictly more correct than the always-quote templates for
case-sensitive names, reserved keywords, and names containing '"'.

Load-bearing property, now pinned by tests: hashed multi-tenant schema names
contain '-' and therefore ALWAYS come out double-quoted. Schema-identifier
rewriting at the pool seam (later in this stack) matches double-quoted schema
tokens in SQL text, so tenant-schema references in plugin SQL must never be
emitted bare. Plain snake_case identifiers (columns, index names, control-
plane schemas that are never rewritten) may now be emitted bare; assertions
updated accordingly.

@pgsql/quotes@17.1.0 is already in the workspace dependency graph (used by
graphile-settings, presigned-url, realtime, pgpm) — zero new install weight.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xqM2VkNbuAmZshK1YNSb
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