Skip to content

feat(db): fail fast on cross-module table access in dev mode#136

Merged
I-am-nothing merged 2 commits into
mainfrom
feat/mh-db-fail-fast
Jul 4, 2026
Merged

feat(db): fail fast on cross-module table access in dev mode#136
I-am-nothing merged 2 commits into
mainfrom
feat/mh-db-fail-fast

Conversation

@I-am-nothing

Copy link
Copy Markdown
Contributor

What

Implements decision-17 §2's dev-mode fail-fast at the SDK query chokepoint (Option 2 — no API break). In dev mode every module shares one owner pool with search_path-only scoping, so a raw SQL read/JOIN against another module's tables silently succeeds locally but can never work deployed, where the per-(app,module) DB role holds no grant on foreign tables — the "works on my machine" trap.

mod.DB / mod.Tx / mod.ModuleDB / mod.ModuleTx now wrap the shared dev pool's db.Querier with a guard that rejects any statement referencing another module's physical m<32hex>_ tables or mod_m<32hex> schema, erroring immediately with a message that names the offending table, its owning module, and ms.DependsOn + n.Table(...) + ms.DependencyDB as the sanctioned path.

Production/deployed connections are untouched — when a platform credential is in ctx the querier passes through unwrapped; Postgres grants are the enforcement there.

How

  • internal/core/db_guard.go: guardQuerier intercepts Exec/Query/QueryRow (QueryRow defers the rejection to Scan, matching pgx's own deferred-error rows). Before scanning, string literals (including dollar-quoted), line comments, and nested block comments are stripped so a foreign table name mentioned in a logged message or comment does not false-positive. Only platform-minted m<32hex> IDs are recognized.
  • devGuardFor decides dev-vs-prod exactly the way resolvePoolFor picks the pool (no platform credential in ctx), per scope: the app-scope credential does not disable the guard for the module-scope path.

Known limits (documented in db_guard.go — this is an honest dev-mode fail-fast, not a security boundary): view/function indirection is not caught, unprefixed foreign names are not caught, and direct db.Open() use bypasses Module.DB entirely.

Also

Second commit fixes decision-17 §3-B comment drift: ExposeTable/Need.Table docs claimed exposed tables live in the producer's cross-app mod_<id> schema; the built grant path targets the per-app tenant schema app_<id>."<prefix><table>". Cross-app module state sharing is explicitly out of scope.

Testing

  • 27 table-driven cases for the SQL scanner: own/foreign tables and schemas, joins, writes, schema-qualified and quoted identifiers, case folding, literals/comments/dollar-quotes (no false positives), mid-identifier non-matches, statements after comments/literals, $n params vs dollar-quotes.
  • Guard wiring tests: foreign statements never reach the underlying querier on any of the three methods; own statements pass through; dev ctx wraps, platform-credential ctx passes through untouched, and module scope with only an app credential still wraps.
  • go build ./..., go vet, full go test ./internal/core/ ./db/ green.

🤖 Generated with Claude Code

I-am-nothing and others added 2 commits July 4, 2026 21:16
In dev mode every module shares one owner pool with search_path-only
scoping, so a raw SQL read/JOIN against another module's tables silently
succeeds locally but can never work deployed (the per-(app,module) DB
role holds no grant on foreign tables) — decision 17 §2's works-on-my-
machine trap.

Close it at the SDK's query chokepoint: mod.DB/Tx/ModuleDB/ModuleTx now
wrap the dev-pool Querier with a guard that rejects any statement
referencing another module's physical m<hex>_ tables (or mod_m<hex>
schema) with an error naming ms.DependencyDB as the sanctioned path.
String literals and comments are stripped before scanning so mentions
don't false-positive. Production connections are untouched — there the
DB role's grants are the enforcement.

Known limits (documented in db_guard.go): view/function indirection is
not caught, unprefixed foreign names are not caught, and direct
db.Open() use bypasses Module.DB entirely. This is an honest dev-mode
fail-fast, not a security boundary.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… reality

The comments claimed exposed tables live in the producer's cross-app
mod_<id> schema; the built grant path targets the per-app tenant schema
app_<id>."<prefix><table>" (decision 17 §3 drift correction). Cross-app
module state sharing is explicitly out of scope.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@I-am-nothing I-am-nothing merged commit 9b9d515 into main Jul 4, 2026
1 check passed
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.

1 participant