Skip to content

fi(adapter-pg): fix $queryRaw regression when using custom schemas#29067

Open
marcelltoth wants to merge 3 commits intoprisma:mainfrom
marcelltoth:fix-pg-raw-query-search-path-regression
Open

fi(adapter-pg): fix $queryRaw regression when using custom schemas#29067
marcelltoth wants to merge 3 commits intoprisma:mainfrom
marcelltoth:fix-pg-raw-query-search-path-regression

Conversation

@marcelltoth
Copy link
Copy Markdown

@marcelltoth marcelltoth commented Jan 22, 2026

Fixes #26930
Fixes #24660

Description

Basically as a commenter pointed out, traditional Prisma engines used to set the SEARCH PATH of the connection used, which functionality did get lost with the driver adapters transition.

This is problematic especially since adapter-pg does have a schema option. Using that alongisde a ?schema={x} query param in the connection string will make Prisma behave in a way where a regular query works as expected, but the equivalent $queryRaw or $queryRawTyped call fails with a relation not found error.

Funnily enough, the way integration tests are set up, they are hitting the exact same problem-scenario. It's just that we didn't have any of those tests attempting to use any of these methods. But check the super trivial test case I added, it's literally just doing a SELECT * over an empty table, and it is failing, because the testing framework does use distinct schemas for all test-cases.

Solution

I solved the issue by porting a modern equivalent of the corresponding Rust code into the new TS-based adapter.

Fortunately the CockroachDB issue has been solved since, so no need to check for flavors. Also we're not parsing the connection string anymore, because with the new API, the schema is nicely passed as an option.

Summary by CodeRabbit

  • Bug Fixes

    • Raw SQL queries now correctly respect custom schema settings, ensuring consistent behavior across query types when using a non-default schema.
  • Tests

    • Added an integration scenario validating raw-query behavior against a custom schema.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Jan 22, 2026

Walkthrough

Adds an acquire handler on the PostgreSQL connection pool so each acquired connection runs SET search_path = "<schema>", public when a custom schema is configured, aligning raw queries with the configured schema.

Changes

Cohort / File(s) Summary
PostgreSQL adapter connection setup
packages/adapter-pg/src/pg.ts
Adds a pool acquire event listener that escapes the configured schema and runs SET search_path = "<schema>", public on each acquired connection with non-fatal error handling.
PostgreSQL integration tests
packages/integration-tests/src/__tests__/integration/postgresql/__scenarios.ts
Adds a scenario "queryRaw - basic validation in custom schema" that creates a posts table in a custom schema and verifies a raw query returns an empty result set.

Sequence Diagram(s)

sequenceDiagram
  participant Adapter as Adapter (PrismaPg)
  participant Pool as Pool
  participant Client as Pool Client
  participant DB as PostgreSQL

  Adapter->>Pool: acquire connection request
  Pool-->>Client: provide client
  Pool->>Client: emit "acquire" (on every acquired client)
  Client->>DB: EXECUTE SET search_path = "<schema>", public
  DB-->>Client: OK / error (non-fatal)
  Adapter->>Client: run user query (e.g., $queryRaw)
  Client->>DB: execute user SQL (uses search_path)
  DB-->>Client: query result
  Client-->>Adapter: result
Loading

Possibly related PRs

  • fix: fix citext test #28906: Fixes the same issue—ensuring adapter-pg sets PostgreSQL search_path for non-default schemas so adapter-driven raw queries use the correct schema.

Suggested labels

lgtm

Suggested reviewers

  • jacek-prisma
  • aqrln
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: fixing $queryRaw regression when using custom schemas in the PostgreSQL adapter.
Linked Issues check ✅ Passed The PR implements the required fix by setting search_path on acquired connections for custom schemas, directly addressing both #26930 and #24660 objectives.
Out of Scope Changes check ✅ Passed All changes are directly related to fixing the search_path regression: adapter code sets search_path on connection acquire, and integration test validates the fix works.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@marcelltoth marcelltoth changed the title Fix the queryRaw search path regression in adapter-pg fi(adapter-pg): Fix $queryRaw regression when using custom schemas Jan 22, 2026
@marcelltoth marcelltoth changed the title fi(adapter-pg): Fix $queryRaw regression when using custom schemas fi(adapter-pg): fix $queryRaw regression when using custom schemas Jan 22, 2026
@CLAassistant
Copy link
Copy Markdown

CLAassistant commented Jan 22, 2026

CLA assistant check
All committers have signed the CLA.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In `@packages/adapter-pg/src/pg.ts`:
- Around line 290-298: The acquire event handler on client (client.on('acquire',
async (conn) => { ... })) risks SQL injection by interpolating
this.options.schema directly and also creates an unhandled promise rejection
because the async listener return is not awaited by pg; fix it by building a
safe quoted identifier for this.options.schema (escape any internal double
quotes by doubling them) and call conn.query without awaiting, attaching
.catch(err => this.onConnectionError?.(err, conn)) so any error is handled via
the existing onConnectionError callback; update the handler to use the escaped
schema value when composing SET search_path and ensure any query rejection is
forwarded to onConnectionError.

Comment on lines +290 to +298
// Since we allow passing a custom DB schema through this.options, we need to set the search path,
// otherwise raw queries will fail when using the non-default search path for the current user,
// leading to diverging behavior between regular and raw queries.
// See https://github.com/prisma/prisma/issues/24660
client.on('acquire', async (conn) => {
if (this.options?.schema) {
await conn.query(`SET search_path = "${this.options.schema}", public`)
}
})
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Jan 22, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🌐 Web query:

PostgreSQL pg library Node.js pool "acquire" event async handler behavior does it await promises

💡 Result:

Short answer: No — pg's Pool emits 'acquire' via Node's EventEmitter and does not await any Promise returned by an async listener. The listener is invoked synchronously and its return value (a Promise) is ignored; rejections can become unhandled unless you handle them or enable captureRejections. [1][2][3]

Sources:

  • node-postgres Pool events (shows pool.on('acquire', ...)). [1]
  • Node.js Events docs: listeners are called synchronously and return values are ignored; captureRejections notes. [2]
  • Community confirmation / discussion. [3]

If you need work to complete before using the client, do that inside your own checkout flow (await pool.connect() and run async setup) rather than relying on an 'acquire' event.


🌐 Web query:

PostgreSQL identifier escaping safe quoting schema names

💡 Result:

Short answer

  • Quote identifiers with double quotes; inside a quoted identifier double quotes are doubled. See PostgreSQL lexical rules. [1]
  • For safe programmatic quoting use quote_ident(name) (in SQL/PL) or format('%I', name) / FORMAT(..., '%I') to produce a properly quoted identifier. For schema-qualified names quote each part: format('%I.%I', schema, table) or quote_ident(schema) || '.' || quote_ident(table). [2][3]

Examples

  • Manual: "My Schema"."My Table" (if a name contains a double quote: "a""b"). [1]
  • In SQL/PLpgSQL: EXECUTE format('SELECT * FROM %I.%I', schemaname, tablename); -- safe. [3]
  • In SQL functions: SELECT quote_ident(my_schema) || '.' || quote_ident(my_table); -- safe. [2]

Sources
[1] PostgreSQL documentation — Lexical structure / quoted identifiers.
[2] PostgreSQL documentation — quote_ident() (string functions).
[3] PostgreSQL FORMAT()/format() usage and %I identifier formatting.


🏁 Script executed:

# Find where the schema option comes from and how it's validated
rg "options.*schema\|schema.*options" packages/adapter-pg/src/ -A 2 -B 2

Repository: prisma/prisma

Length of output: 39


🏁 Script executed:

# Check if there are existing patterns for setting search_path or escaping in the codebase
rg "search_path\|escapId\|quote.*identifier" packages/adapter-pg/ -i

Repository: prisma/prisma

Length of output: 39


🏁 Script executed:

# Read the pg.ts file to understand schema source and validation
cat packages/adapter-pg/src/pg.ts | head -100

Repository: prisma/prisma

Length of output: 3052


🏁 Script executed:

# Find the options definition and schema property
rg "schema" packages/adapter-pg/src/pg.ts -B 5 -A 5

Repository: prisma/prisma

Length of output: 1249


🏁 Script executed:

# Check if there's type definition for options
fd "index.d.ts|types.ts" packages/adapter-pg/src/

Repository: prisma/prisma

Length of output: 39


Fix SQL injection and unhandled rejection in acquire event handler.

The code directly interpolates schema without escaping, risking SQL injection. Additionally, the async handler's promise rejection is unhandled—pg's acquire event does not await listener return values, causing rejections to silently fail.

Escape the schema identifier using PostgreSQL's double-quote escaping (doubling internal quotes), remove the await, and explicitly handle errors using .catch() to integrate with the existing onConnectionError callback.

🛠️ Proposed fix
-    client.on('acquire', async (conn) => {
-      if (this.options?.schema) {
-        await conn.query(`SET search_path = "${this.options.schema}", public`)
-      }
-    })
+    const schema = this.options?.schema
+    client.on('acquire', (conn) => {
+      if (!schema) return
+      const escapedSchema = `"${schema.replace(/"/g, '""')}"`
+      void conn
+        .query(`SET search_path = ${escapedSchema}, public`)
+        .catch((err) => {
+          debug(`Error setting search_path to ${escapedSchema}: ${err.message} %O`, err)
+          this.options?.onConnectionError?.(err)
+        })
+    })
🤖 Prompt for AI Agents
In `@packages/adapter-pg/src/pg.ts` around lines 290 - 298, The acquire event
handler on client (client.on('acquire', async (conn) => { ... })) risks SQL
injection by interpolating this.options.schema directly and also creates an
unhandled promise rejection because the async listener return is not awaited by
pg; fix it by building a safe quoted identifier for this.options.schema (escape
any internal double quotes by doubling them) and call conn.query without
awaiting, attaching .catch(err => this.onConnectionError?.(err, conn)) so any
error is handled via the existing onConnectionError callback; update the handler
to use the escaped schema value when composing SET search_path and ensure any
query rejection is forwarded to onConnectionError.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

FWIW I did consider these both, I decided to go this way in the end, but I'm open to (human) maintainer preference opinions.

Regarding the injection, I don't think this matters at all, whoever can control this.options has control over the whole object anyway.

And then re. error handing: I didn't think there was any super-useful thing I could do. onConnectionError is almost certainly inappropriate, but I can add the debug logging if you feel like that helps.

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.

@marcelltoth, you're right on both counts—I appreciate you thinking through these tradeoffs.

On identifier escaping: Fair point that if someone controls this.options, the game is already over security-wise. That said, escaping the identifier would still be valuable for robustness—it prevents bugs when schema names contain double quotes, spaces, or other special characters. PostgreSQL requires such identifiers to have internal quotes doubled ("my""schema"). It's less about security and more about not breaking when someone uses a valid but unusual schema name.

On error handling: Agreed that onConnectionError doesn't fit here semantically. Adding debug logging would at least make failures visible during development/troubleshooting. Since the acquire event doesn't await, any rejection would otherwise be silent (or an unhandled rejection warning), and the connection would be used with the wrong search path.

A minimal approach:

client.on('acquire', (conn) => {
  if (this.options?.schema) {
    const schema = this.options.schema.replace(/"/g, '""') // escape internal quotes
    conn.query(`SET search_path = "${schema}", public`).catch((err) => {
      debug(`Failed to set search_path: ${err.message}`)
    })
  }
})

But I'll defer to maintainer preference—if they're comfortable with the current implementation, that works too!

@marcelltoth marcelltoth force-pushed the fix-pg-raw-query-search-path-regression branch from 34d4e5b to 0362aa7 Compare January 22, 2026 15:29
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/adapter-pg/src/pg.ts (1)

290-313: Remove the acquire listener when external pools are kept active.

When an external pool is provided and disposeExternalPool is false, connect() registers an acquire listener (line 294) but only removes the error listener on disposal (line 309). The acquire listener remains attached to the pool, continuing to execute on every connection acquisition and retaining adapter references indefinitely. This can interfere with other users of the same external pool or persist unintended side effects after the adapter is disposed.

Store the listener in a variable (as done for onIdleClientError) and remove it in the non-dispose branch:

Proposed fix
+    const onAcquire = async (conn: pg.PoolClient) => {
+      if (this.options?.schema) {
+        const schemaEscaped = this.options.schema.replace(/"/g, '""')
+        conn.query(`SET search_path = "${schemaEscaped}", public`).catch((err) => {
+          debug(`Failed to set search_path: ${err.message}`)
+        })
+      }
+    }
-    client.on('acquire', async (conn) => {
-      if (this.options?.schema) {
-        const schemaEscaped = this.options.schema.replace(/"/g, '""')
-        conn.query(`SET search_path = "${schemaEscaped}", public`).catch((err) => {
-          debug(`Failed to set search_path: ${err.message}`)
-        })
-      }
-    })
+    client.on('acquire', onAcquire)
         } else {
           this.externalPool.removeListener('error', onIdleClientError)
+          this.externalPool.removeListener('acquire', onAcquire)
         }

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.

Schema isn't included in queries when using TypedSQL in combination with Driver Adapter $queryRaw ignores schema when using @prisma/adapter-pg

2 participants