Skip to content

perf(client-engine): split render and data-mapper prerequisites - #29658

Open
tensordreams wants to merge 12 commits into
v7from
prisma-client-perf-render-datamapper-prereqs
Open

tensordreams wants to merge 12 commits into
v7from
prisma-client-perf-render-datamapper-prereqs

Conversation

@tensordreams

@tensordreams tensordreams commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Optimizes Prisma Client’s query-plan hot path ahead of the compact query-plan consumer by reducing allocations in SQL rendering and mapping database rows directly into result structures.

Changes

  • SQL rendering: Adds static and flat-template fast paths, caches flat renderings, renders tuple placeholders without intermediate arrays, and avoids rebuilding chunk plans when parameters already fit the database limit.
  • Result mapping: Maps simple result sets directly from rows, reuses field mappings, and caches mappings by stable column shape so adapters with fresh metadata arrays still benefit.
  • Query execution: Shares one query execution and row-merge path across ordinary and directly mapped queries while preserving SQL comments, tracing, raw-query error handling, and stack-safe row aggregation.
  • Coverage: Exercises flat, nested-row, JSON value-object, duplicate-column, missing-column, cache-shape, and SQL rendering behavior.
  • Stack: This Prisma-only prerequisite remains the base for prisma-client-perf-compact-plan-format; the matching engines producer is prisma-client-perf-compact-plan-format-engines.

Why

The compact query-plan work needs the runtime hot path to avoid avoidable iterator, array, object, and serialization overhead independently of the internal plan-format migration. Keeping these prerequisites separate makes the performance behavior reviewable without requiring a custom engines branch.

Validation

  • pnpm --filter @prisma/client-engine-runtime exec vitest run src/interpreter/data-mapper.test.ts src/interpreter/query-interpreter.test.ts src/interpreter/render-query.test.ts
  • pnpm --filter @prisma/client-engine-runtime test — 222 tests passed
  • pnpm --filter @prisma/client-engine-runtime build
  • pnpm lint — 0 errors (repository warnings unchanged)

@coderabbitai

coderabbitai Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Adds direct SQL result-set mapping into Prisma objects with recursive nested and JSON value handling, cached column mappings, missing-column errors, and validation tests. Refactors query execution to merge chunks, preserve insert IDs, instrument queries, translate errors, and directly map non-raw query data maps. Reworks template SQL rendering and parameter chunking with cached flat templates, explicit placeholders, tuple/list expansion, and validation.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.23% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title is concise and accurately reflects the main render and data-mapper performance changes.
Description check ✅ Passed The description is clearly related to the SQL rendering, result mapping, and query execution changes in the PR.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch prisma-client-perf-render-datamapper-prereqs
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch prisma-client-perf-render-datamapper-prereqs

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.

@github-actions

github-actions Bot commented Jun 25, 2026 •

Copy link
Copy Markdown
Contributor

size-limit report 📦

Path Size
packages/client/runtime/index-browser.js 2.29 KB (0%)
packages/client/runtime/index-browser.d.ts 3.37 KB (0%)
packages/cli/build/index.js 110 B (0%)
packages/client/prisma-client-0.0.0.tgz 26.94 MB (+0.09% 🔺)
packages/cli/prisma-0.0.0.tgz 14.05 MB (0%)
packages/bundle-size/da-workers-libsql/output.tgz 1.34 MB (+0.11% 🔺)
packages/bundle-size/da-workers-neon/output.tgz 1.4 MB (+0.1% 🔺)
packages/bundle-size/da-workers-pg/output.tgz 1.4 MB (+0.13% 🔺)
packages/bundle-size/da-workers-planetscale/output.tgz 1.34 MB (+0.1% 🔺)
packages/bundle-size/da-workers-d1/output.tgz 1.32 MB (+0.11% 🔺)

@codspeed

codspeed Bot commented Jun 25, 2026 •

Copy link
Copy Markdown

Merging this PR will improve performance by 60.05%

⚠️ Different runtime environments detected

Some benchmarks with significant performance changes were compared across different runtime environments,
which may affect the accuracy of the results.

Open the report in CodSpeed to investigate

⚡ 1 improved benchmark
✅ 16 untouched benchmarks
⏩ 30 skipped benchmarks1

Performance Changes

Benchmark BASE HEAD Efficiency
⚡ interpreter: findUnique 610.9 µs 381.7 µs +60.05%

Tip

Curious why this is faster? Comment @codspeedbot explain why this is faster on this PR, or directly use the CodSpeed MCP with your agent.


Comparing prisma-client-perf-render-datamapper-prereqs (5696906) with main (f2b3abd)

Open in CodSpeed

Footnotes

  1. 30 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@tensordreams
tensordreams changed the base branch from main to v7 July 21, 2026 12:04
@tensordreams
tensordreams changed the base branch from v7 to main July 21, 2026 15:10

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
packages/client-engine-runtime/src/interpreter/query-interpreter.ts (1)

187-212: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Unify query case with the new #executeQuery helper (duplicated execution/merge logic).

#executeQuery (409-436) re-implements the same render → comment → execute → merge-rows logic already inline in the query case (187-212), except it also skips comment application when there's no sqlcommenter and skips the tracing wrapper when instrumentation isn't in use. Keeping both means the query case never benefits from those optimizations, and the two copies can silently drift.

♻️ Proposed refactor
       case 'query': {
-        const queries = renderQuery(node.args, context.scope, context.generators, this.#maxChunkSize())
-
-        let results: SqlResultSet | undefined
-        for (const query of queries) {
-          const commentedQuery = applyComments(query, context.sqlCommenter)
-          const result = await this.#withQuerySpanAndEvent(commentedQuery, context.queryable, () =>
-            context.queryable
-              .queryRaw(cloneObject(commentedQuery))
-              .catch((err) =>
-                node.args.type === 'rawSql' ? rethrowAsUserFacingRawError(err) : rethrowAsUserFacing(err),
-              ),
-          )
-          if (results === undefined) {
-            results = result
-          } else {
-            results.rows.push(...result.rows)
-            results.lastInsertId = result.lastInsertId
-          }
-        }
+        const results = await this.#executeQuery(node.args, context)

         return {
           value: node.args.type === 'rawSql' ? this.#rawSerializer(results!) : this.#serializer(results!),
           lastInsertId: results?.lastInsertId,
         }
       }

Also applies to: 408-436

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-engine-runtime/src/interpreter/query-interpreter.ts` around
lines 187 - 212, Replace the inline render, comment, execution, and
result-merging logic in the query case with the existing `#executeQuery` helper.
Pass the query node arguments, scope, generators, queryable, and sql commenter
through that helper, then serialize its merged result and preserve lastInsertId
handling for rawSql and regular queries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client-engine-runtime/src/interpreter/data-mapper.test.ts`:
- Around line 8-86: Add tests around buildResultSetFieldMappings and
applyDataMapToResultSet covering nested rowObject mappings, valueObject/JSON
column mappings, and the DataMapperError path when a required column is missing.
Assert the recursively mapped output and the expected error, while preserving
the existing flat-field and cache-order coverage.

---

Outside diff comments:
In `@packages/client-engine-runtime/src/interpreter/query-interpreter.ts`:
- Around line 187-212: Replace the inline render, comment, execution, and
result-merging logic in the query case with the existing `#executeQuery` helper.
Pass the query node arguments, scope, generators, queryable, and sql commenter
through that helper, then serialize its merged result and preserve lastInsertId
handling for rawSql and regular queries.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 02f37528-fab4-463b-84a1-318c38474adb

📥 Commits

Reviewing files that changed from the base of the PR and between a6d0155 and 33c51d6.

📒 Files selected for processing (5)
  • AGENTS.md
  • packages/client-engine-runtime/src/interpreter/data-mapper.test.ts
  • packages/client-engine-runtime/src/interpreter/data-mapper.ts
  • packages/client-engine-runtime/src/interpreter/query-interpreter.ts
  • packages/client-engine-runtime/src/interpreter/render-query.ts

Comment thread packages/client-engine-runtime/src/interpreter/data-mapper.test.ts
@tensordreams
tensordreams force-pushed the prisma-client-perf-render-datamapper-prereqs branch from 33c51d6 to 5696906 Compare July 23, 2026 21:08
@tensordreams
tensordreams marked this pull request as ready for review July 23, 2026 21:08

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 2

Caution

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

⚠️ Outside diff range comments (1)
packages/client-engine-runtime/src/interpreter/render-query.ts (1)

438-506: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Handle tied oversized fragments together when chunking.

The current tie-break only splits the first maxParamsPerFragment; a second fragment with the same parameter count is appended unsplit to every chunk. Add a regression for two same-size parameterTuple fragments that together exceed maxChunkSize, or split/group both oversized fragments so no final chunk exceeds the limit.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-engine-runtime/src/interpreter/render-query.ts` around lines
438 - 506, Update the chunking logic in the parameterTuple and
parameterTupleList branches so all fragments tied at maxParamsPerFragment are
split or grouped consistently, rather than only the first tied fragment. Ensure
every generated chunk remains at or below maxChunkSize when multiple same-sized
oversized fragments together exceed the limit, and add a regression covering two
equal-size parameterTuple fragments.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/client-engine-runtime/src/interpreter/render-query.ts`:
- Around line 508-534: Extract the shared parameter bounds and tuple-shape
validation from renderTemplateSql and the accessors getParam, getTupleParam, and
getTupleListParam into common helpers. Update both call sites to reuse those
helpers while preserving the existing error messages and validation behavior,
including empty tuple-list and non-tuple element checks.
- Around line 102-163: Add a bounds validation in getFlatTemplateSqlRendering
after counting parameter fragments and before constructing the rendering: reject
any case where paramCount exceeds dbQuery.argTypes.length with the file’s
established malformed-query error behavior, rather than calling copyArgTypes
with an oversized count. Preserve the existing exact-length and shorter-count
handling for valid plans.

---

Outside diff comments:
In `@packages/client-engine-runtime/src/interpreter/render-query.ts`:
- Around line 438-506: Update the chunking logic in the parameterTuple and
parameterTupleList branches so all fragments tied at maxParamsPerFragment are
split or grouped consistently, rather than only the first tied fragment. Ensure
every generated chunk remains at or below maxChunkSize when multiple same-sized
oversized fragments together exceed the limit, and add a regression covering two
equal-size parameterTuple fragments.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 765b9150-f698-4f84-9ab5-3f8fdf79e0ed

📥 Commits

Reviewing files that changed from the base of the PR and between 33c51d6 and 5696906.

📒 Files selected for processing (5)
  • AGENTS.md
  • packages/client-engine-runtime/src/interpreter/data-mapper.test.ts
  • packages/client-engine-runtime/src/interpreter/data-mapper.ts
  • packages/client-engine-runtime/src/interpreter/query-interpreter.ts
  • packages/client-engine-runtime/src/interpreter/render-query.ts

Comment on lines +102 to +163
function getFlatTemplateSqlRendering(dbQuery: DeepReadonly<QueryPlanDbQuery>): FlatTemplateSqlRendering | undefined {
const cached = flatTemplateSqlCache.get(dbQuery)
if (cached !== undefined) {
return cached ?? undefined
}

if (dbQuery.type !== 'templateSql') {
flatTemplateSqlCache.set(dbQuery, null)
return undefined
}

let sql = ''
let placeholderNumber = 1
let paramCount = 0

for (const fragment of dbQuery.fragments) {
switch (fragment.type) {
case 'stringChunk':
sql += fragment.chunk
break

case 'parameter':
sql += formatPlaceholder(dbQuery.placeholderFormat, placeholderNumber++)
paramCount++
break

case 'parameterTuple':
case 'parameterTupleList':
flatTemplateSqlCache.set(dbQuery, null)
return undefined

default:
assertNever(fragment, 'Invalid fragment type')
}
}

const rendering: FlatTemplateSqlRendering = {
sql,
paramCount,
argTypes:
paramCount === dbQuery.argTypes.length
? (dbQuery.argTypes as ArgType[])
: copyArgTypes(dbQuery.argTypes, paramCount),
}
flatTemplateSqlCache.set(dbQuery, rendering)
return rendering
}

function evaluateArgs(
args: readonly unknown[],
scope: ScopeBindings,
generators: GeneratorRegistrySnapshot,
): unknown[] {
const result = new Array<unknown>(args.length)

for (let i = 0; i < args.length; i++) {
result[i] = evaluateArg(args[i], scope, generators)
}

return result
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Add a bounds guard for paramCount vs argTypes.length.

In getFlatTemplateSqlRendering, argTypes: paramCount === dbQuery.argTypes.length ? (dbQuery.argTypes as ArgType[]) : copyArgTypes(dbQuery.argTypes, paramCount) assumes paramCount <= dbQuery.argTypes.length. If a malformed query plan ever produces more parameter fragments than argTypes entries, copyArgTypes (Line 201-207) will silently read past the array bounds and fill the result with undefined, instead of raising a clear "malformed query template" error like the sibling checks elsewhere in this file (e.g. Line 72-74, Line 229-231).

🛡️ Proposed fix
 function copyArgTypes(argTypes: DeepReadonly<DynamicArgType[]>, length: number): ArgType[] {
+  if (length > argTypes.length) {
+    throw new Error(`Malformed query template. Fragments attempt to read over ${argTypes.length} parameter types.`)
+  }
   const result = new Array<ArgType>(length)
   for (let i = 0; i < length; i++) {
     result[i] = argTypes[i] as ArgType
   }
   return result
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function getFlatTemplateSqlRendering(dbQuery: DeepReadonly<QueryPlanDbQuery>): FlatTemplateSqlRendering | undefined {
const cached = flatTemplateSqlCache.get(dbQuery)
if (cached !== undefined) {
return cached ?? undefined
}
if (dbQuery.type !== 'templateSql') {
flatTemplateSqlCache.set(dbQuery, null)
return undefined
}
let sql = ''
let placeholderNumber = 1
let paramCount = 0
for (const fragment of dbQuery.fragments) {
switch (fragment.type) {
case 'stringChunk':
sql += fragment.chunk
break
case 'parameter':
sql += formatPlaceholder(dbQuery.placeholderFormat, placeholderNumber++)
paramCount++
break
case 'parameterTuple':
case 'parameterTupleList':
flatTemplateSqlCache.set(dbQuery, null)
return undefined
default:
assertNever(fragment, 'Invalid fragment type')
}
}
const rendering: FlatTemplateSqlRendering = {
sql,
paramCount,
argTypes:
paramCount === dbQuery.argTypes.length
? (dbQuery.argTypes as ArgType[])
: copyArgTypes(dbQuery.argTypes, paramCount),
}
flatTemplateSqlCache.set(dbQuery, rendering)
return rendering
}
function evaluateArgs(
args: readonly unknown[],
scope: ScopeBindings,
generators: GeneratorRegistrySnapshot,
): unknown[] {
const result = new Array<unknown>(args.length)
for (let i = 0; i < args.length; i++) {
result[i] = evaluateArg(args[i], scope, generators)
}
return result
}
function copyArgTypes(argTypes: DeepReadonly<DynamicArgType[]>, length: number): ArgType[] {
if (length > argTypes.length) {
throw new Error(`Malformed query template. Fragments attempt to read over ${argTypes.length} parameter types.`)
}
const result = new Array<ArgType>(length)
for (let i = 0; i < length; i++) {
result[i] = argTypes[i] as ArgType
}
return result
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-engine-runtime/src/interpreter/render-query.ts` around lines
102 - 163, Add a bounds validation in getFlatTemplateSqlRendering after counting
parameter fragments and before constructing the rendering: reject any case where
paramCount exceeds dbQuery.argTypes.length with the file’s established
malformed-query error behavior, rather than calling copyArgTypes with an
oversized count. Preserve the existing exact-length and shorter-count handling
for valid plans.

Comment on lines +508 to +534
function getParam(params: unknown[], index: number): unknown {
if (index >= params.length) {
throw new Error(`Malformed query template. Fragments attempt to read over ${params.length} parameters.`)
}
return params[index]
}

function getTupleParam(params: unknown[], index: number): unknown[] {
const value = getParam(params, index)
return Array.isArray(value) ? value : [value]
}

function getTupleListParam(params: unknown[], index: number): unknown[][] {
const value = getParam(params, index)
if (!Array.isArray(value)) {
throw new Error(`Malformed query template. Tuple list expected.`)
}
if (value.length === 0) {
throw new Error(`Malformed query template. Tuple list cannot be empty.`)
}
for (let i = 0; i < value.length; i++) {
if (!Array.isArray(value[i])) {
throw new Error(`Malformed query template. Tuple expected.`)
}
}
return value
}

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.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate validation logic between chunk accessors and renderTemplateSql.

getParam/getTupleParam/getTupleListParam re-implement the exact same bounds/shape checks ("Fragments attempt to read over N parameters", "Tuple list expected", "Tuple list cannot be empty", "Tuple expected") already inlined in renderTemplateSql (Line 229-231, Line 241-243, Line 256-266). Both blocks were introduced in this same refactor, so extracting a shared validation helper now avoids the two copies silently diverging later (e.g. if an error message changes in one place but not the other).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/client-engine-runtime/src/interpreter/render-query.ts` around lines
508 - 534, Extract the shared parameter bounds and tuple-shape validation from
renderTemplateSql and the accessors getParam, getTupleParam, and
getTupleListParam into common helpers. Update both call sites to reuse those
helpers while preserving the existing error messages and validation behavior,
including empty tuple-list and non-tuple element checks.

@tensordreams
tensordreams changed the base branch from main to v7 July 28, 2026 08:51

This branch has not been deployed

No deployments
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