PostgreSQL

Prisma $queryRaw Fails With Variables Inside Single Quotes

Why interval and LIKE patterns break in Prisma raw SQL, the rewrites that work on Prisma 6 and 7, and how to prove the query now uses your value.

Mahdi Ben RhoumaMahdi Ben Rhouma
8 min read

TL;DR#

In a Prisma tagged template, every ${value} becomes a bind parameter ($1, $2, …). If you put it inside single quotes — interval '${days} day' or LIKE '%${term}%' — PostgreSQL sees those characters as part of a string literal, not as a parameter, and the Prisma raw query docs say plainly that template variables cannot be used inside SQL string literals. The statement then declares fewer parameters than Prisma sends. Prisma 7 with the pg adapter fails with bind message supplies 1 parameters, but prepared statement "" requires 0; a Stack Overflow report on Prisma 3.3.0 shows P1016; and in our test Prisma 6 raised no error at all and quietly used the wrong value. The fix is always the same: move the value out of the quotes and let SQL build the literal around it.

The error#

The original Stack Overflow question runs a daily status count over the last N days:

ts
const rows = await prisma.$queryRaw`
  select count(id), status, "createdAt"::date
  from "ProjectLog"
  where "projectId" = (select id from "Project" where slug = ${id})
    and "createdAt" > current_date - interval '${interval} day'
  group by "createdAt"::date, status`;

Prisma's query log in that question shows exactly what was sent:

text
Query: select count(id) as count, status,  "createdAt"::date from "ProjectLog" where "projectId" = (select id from "Project" where slug = $1) and "createdAt" >  current_date - interval '$2 day' and key notnull group by "createdAt"::date, status
Param: ["main","30"]
PrismaClientKnownRequestError:
Invalid `prisma.queryRaw()` invocation:
 
  Your raw query had an incorrect number of parameters. Expected: `1`, actual: `2`.
  code: 'P1016',
  clientVersion: '3.3.0',
  meta: { expected: 1, actual: 2 }

On a current stack the message is different. We reproduced the pattern on 1 October 2026 with Prisma 7, its @prisma/adapter-pg driver adapter and the official postgres:18 Docker image. This is the output of the LIKE variant, status LIKE '%${term}%':

text
prisma: 7.10.0, @prisma/adapter-pg: 7.10.0, pg: 8.23.1, PostgreSQL 18.6
 
PrismaClientKnownRequestError P2010
Invalid `prisma.$queryRaw()` invocation:
Raw query failed. Code: `08P01`. Message: `bind message supplies 1 parameters, but prepared statement "" requires 0`

The thrown error carries Prisma's code P2010 (raw query failed), with the PostgreSQL code 08P01 inside it. A query with two quoted variables reports supplies 2 parameters instead.

Why it happens#

$queryRaw is a tagged template: Prisma keeps the SQL text, swaps each ${…} for a numbered placeholder and sends the values separately as a prepared statement. That is what makes it safe from SQL injection. It also means a placeholder only works where PostgreSQL expects a value.

Inside a string constant there is no value position — just text. In the query log above, interval '$2 day' hands the text $2 day to the interval parser, and a pattern such as '%$1%' matches a literal dollar sign followed by a digit. The query text ends up with fewer placeholders than the number of values Prisma binds. What happens next depends on the version:

Stackinterval '${days} days' with days = 30
Prisma 3.3.0 (from the Stack Overflow report)P1016 — expected 1 parameter, got 2
Prisma 6, default query engine (our test)No error. The interval used was the wrong one
Prisma 7 + @prisma/adapter-pg (our test)08P01 — the bind message supplies more parameters than the statement requires

The Prisma 6 row is the dangerous one. PostgreSQL's interval input accepts the leftover text and turns it into a one-day interval, which psql shows directly:

text
postgres=# select interval '$1 days';
 interval
----------
 1 day

So on Prisma 6 the dashboard query counts a much shorter window than the one you asked for, and returns plausible-looking numbers. Our test table held three rows, one of them created ten days earlier. This is what the quoted query and the LIKE variant returned on Prisma 6:

text
prisma: 6.19.3, @prisma/client: 6.19.3, PostgreSQL 18.6
 
quoted interval   -> [{"w":"1 day","n":2}]          (the 10-day-old row is missing)
quoted LIKE       -> []                             (the "failed" row is missing)

If you are on Prisma 6 and your numbers look low rather than broken, check for quoted placeholders before anything else.

Fix#

Every fix below has the same shape: the variable sits outside the quotes as a whole value, and SQL does the string or interval work. The output after each fix is from the same test table on both Prisma 6 and Prisma 7.

1. Intervals: build the interval from a number#

make_interval from the PostgreSQL date/time functions takes the number of days as a named argument:

ts
const days = 30;
 
const rows = await prisma.$queryRaw`
  select count(id)::int as count, status, "createdAt"::date as day
  from "ProjectLog"
  where "createdAt" > now() - make_interval(days => ${days}::int)
  group by "createdAt"::date, status`;

Keep the ::int cast. Without it, Prisma 6 failed with function make_interval(days => bigint) does not exist, because Prisma sends JavaScript integers to PostgreSQL as INT8 and make_interval takes an integer. Prisma 7 accepted it without the cast in our test, but the cast costs nothing and keeps the query portable across both.

2. Intervals: multiply a fixed unit#

If you prefer not to name the unit in a function call, multiply a constant interval. The quoted part contains no variable, so it stays a plain literal:

ts
const rows = await prisma.$queryRaw`
  select count(id)::int as count
  from "ProjectLog"
  where "createdAt" > now() - ${days} * interval '1 day'`;

3. Intervals: pass the whole string and cast it#

This is the approach of the first answer on the question. Build the full text in JavaScript and cast the parameter:

ts
const window = `${days} days`;
 
const rows = await prisma.$queryRaw`
  select count(id)::int as count
  from "ProjectLog"
  where "createdAt" > now() - ${window}::interval`;

All three interval forms gave the same result on both versions:

text
make_interval(days => ${days}::int)  -> [{"w":"30 days","n":3}]
${days} * interval '1 day'           -> [{"w":"30 days","n":3}]
${window}::interval                  -> [{"w":"30 days","n":3}]

4. LIKE and ILIKE: put the wildcards in the value#

The same bug hits search queries. Put the % characters in the variable, as the Prisma docs show for parameterised ILIKE, or concatenate in SQL:

ts
const term = 'fail';
 
// wildcards inside the value
const a = await prisma.$queryRaw`
  select id, status from "ProjectLog" where status ilike ${`%${term}%`}`;
 
// or concatenation in SQL
const b = await prisma.$queryRaw`
  select id, status from "ProjectLog" where status ilike '%' || ${term} || '%'`;

Both returned the failed row on Prisma 6 and Prisma 7. One more thing that is easy to miss: % and _ typed by the user are still wildcards inside the bound value. If the search box must match them literally, escape them before adding your own wildcards (PostgreSQL's LIKE uses backslash as the default escape character).

What not to do: Prisma.raw with request data#

The third answer on the question wraps the quoted part in Prisma.raw. It works, because Prisma.raw inserts text instead of a placeholder — which is also why the Prisma docs list it under unsafe use of $queryRaw. Use it only for values your own code produces, such as a constant from an allow-list, never for a query-string parameter. The same applies to $queryRawUnsafe with string building: if you need it, pass the values as separate positional arguments instead of interpolating them.

Verify the fix#

Do not trust "no error" as proof — on Prisma 6 the broken query did not error either. Ask PostgreSQL what interval it actually received:

ts
const check = await prisma.$queryRaw<{ window: string }[]>`
  select (${days} * interval '1 day')::text as window`;
 
console.log(check); // [ { window: '30 days' } ]

Run the same check with the old quoted form if you want to see the bug in one line: Prisma 6 answers with a one-day interval and Prisma 7 throws the 08P01 error. For search queries, run the query against a term you know exists and assert a non-zero count. Better still, seed one row just inside the window you expect and one just outside it, as in the test above: a wrong window changes the count.

You can also turn on query logging with log: ['query'] in the PrismaClient options (next to the adapter on Prisma 7) and read the SQL: every $1, $2 should stand on its own, never between quotes.

One email a month — the latest explained errors

What the site published that month — mostly Next.js, Supabase and Node errors, explained. You confirm by email; unsubscribe any time in one click.

When the error moves to the parameter type#

Once the placeholder is outside the quotes, the next failure is usually about its type, not its position. function make_interval(days => bigint) does not exist (code 42883) means a JavaScript number arrived as INT8 where the function expects a smaller integer. Cast the placeholder (${days}::int) rather than changing the function. The grouping half of the original query — "createdAt"::date — is covered in extracting a date from a timestamp in PostgreSQL. If the query runs through Supabase's pooled port on Prisma 6 and starts failing with prepared statement "s0" already exists, that is a pooler problem rather than a placeholder problem, explained in the Prisma prepared statement s0 fix. And if Prisma reports invalid port number although the port is correct, a special character in the password is breaking the URL: see the invalid port number fix.

Raw SQL in Prisma stays safe only while every variable is a whole value: an operand, a function argument, a comparison side. Once a variable lands inside a quoted string, Prisma cannot parameterise it and PostgreSQL will not read it as one. Keep the quotes for fixed text, keep the variables outside them, and check the interval or pattern PostgreSQL actually used before you trust the numbers.

Further reading: Prisma $queryRaw TS2345: 'string' not assignable to Sql.

Frequently Asked Questions

|

Have more questions? Contact us

Still stuck? Paste your error.

Paste the error message or stack trace — we check it against every fix and tool on the site.

Matching happens in your browser. When you search, a cleaned error signature is sent so we can spot errors we do not cover yet — never the raw text, your IP address or your e-mail. Do not paste passwords or keys. Details

Written by

Mahdi Ben Rhouma
Mahdi Ben Rhouma

Full-Stack Dev — Next.js & Supabase

Solo developer building SaaS products with Next.js and Supabase. Writing about production patterns the official docs skip.

Remote