Skip to content

feat(prisma): add prisma adapter - #838

Merged
tada5hi merged 4 commits into
masterfrom
feat/prisma-adapter
Jul 26, 2026
Merged

tada5hi merged 4 commits into
masterfrom
feat/prisma-adapter

Conversation

@tada5hi

@tada5hi tada5hi commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Adds @rapiq/prisma, the first of the additional ORM adapters from the plan-023 direction doc, plus the core plan transform it (and the next adapters) build on.

What it does

Serializes a parsed Query into a Prisma findMany argument object. A pure, stateless serializer: execute maps a value to a value, and @prisma/client is neither a runtime nor a peer dependency (devDependency only, for the engine tests). Queries compose before serialization (mergeQueries, filters.and()), so there is no accumulation API.

const adapter = new PrismaAdapter<Prisma.UserFindManyArgs>({
    provider: 'postgresql',
    metadata: defineMetadata(Prisma.dmmf.datamodel, 'User'),
});

const { args, pagination } = adapter.execute(query);
const users = await prisma.user.findMany(args);
Query parameter Prisma argument
filters where
fields select
relations include, or a nested select when fields are picked
sort orderBy
pagination take / skip

Architecture: pure passes, semantics stay in core

The where is produced by a pipeline: planCondition -> distributeNegation (new in @rapiq/core) -> quantifier factoring -> a leaf-literal table.

distributeNegation eliminates group negation by pushing it to the leaves: De Morgan over compounds, leaf twins toggled, not(ordering) rewritten as the dual operator OR a null check (complements derived from the semantics table, not hardcoded). Core keeps group negation for sql/memory, which render it two-valued cheaply; backends without a two-valued NOT (prisma now, Drizzle/MikroORM next per plan 023) consume the transform instead of re-deriving operator semantics locally. Prisma's three-valued not/NOT is never relied upon.

Cross-backend semantics hold exactly

Two contracts that are easy to get wrong, both pinned by probing @rapiq/memory and by the engine suite:

  • Same-element binding. filter[items.title] + filter[items.color] bind to the same joined row in sql/typeorm/memory. The renderer factors conditions sharing a to-many path into ONE some scope; mixed root/relation trees are expanded distributively first ( distributes over OR; capped at 64 conjuncts with a typed error). A record satisfying the conditions only on different elements does not match, on any backend.
  • not(elemMatch(c)) is per-binding. Group negation applies per binding with the quantifier outermost (sql wraps CASE per join row), so the complement is some(not c) plus the empty-collection arm, NOT none(c). Every none: {}/absence arm follows one rule: the interior evaluated at the all-null binding an empty collection contributes.

provider and metadata are required: each metadata fact (relation, cardinality, nullability, string-typedness) changes what a valid prisma filter looks like, so a wrong guess is a validation error rather than degradation.

Parity is measured, not modelled

npm run test:db runs the matrix through a real Prisma client and cross-checks every condition three ways: engine, @rapiq/memory, and an in-test evaluator. SQLite by default; PostgreSQL under DB_TYPE=postgres in the existing tests-db job (the only connector where mode: 'insensitive' exists, so the case contract and the ILIKE-wildcard veto are measured there). The fixtures include a mixed-element record that would have caught both semantics bugs above; earlier fixtures were too weak to distinguish them.

Measured on Postgres 18 / Prisma 6.19.3: { equals: 'a_b', mode: 'insensitive' } lowers to ILIKE and wildcards (a%b matches every row), while in/notIn never do, so the wildcard veto applies to the equality family only.

Known limitations

Typed AdapterError rather than an approximation: regex, mod, size, the $this marker, elemMatch on a to-one relation, and filter trees beyond the expansion ceiling. To-many orderBy is not expressible in Prisma; SQLite compares case-sensitively (measured, documented).

Notes

  • Release-please wiring (component and manifest) is included.
  • Docs: package page, README in the docs: give sub-package READMEs a consistent, richer layout #836 layout, family-table rows in every sibling README, and references from the overview, installation and executing-queries pages.
  • Schema derivation from DMMF (the plan-017 analogue) is deliberately deferred, same staging as typeorm.

Copilot AI review requested due to automatic review settings July 25, 2026 22:55

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@coderabbitai

coderabbitai Bot commented Jul 25, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@tada5hi, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: ab8c1056-e259-499c-a719-d7312f575414

📥 Commits

Reviewing files that changed from the base of the PR and between 9242976 and a114010.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (63)
  • .agents/architecture.md
  • .agents/structure.md
  • .github/workflows/main.yml
  • .release-please-manifest.json
  • README.md
  • packages/codec-url/README.md
  • packages/core/README.md
  • packages/core/src/parameter/filters/plan/distribute.ts
  • packages/core/src/parameter/filters/plan/index.ts
  • packages/core/test/unit/parameter/filters-plan-distribute.spec.ts
  • packages/docs/.vitepress/config.mjs
  • packages/docs/.vitepress/theme/components/PackageLayers.vue
  • packages/docs/guide/executing-queries.md
  • packages/docs/guide/installation.md
  • packages/docs/packages/index.md
  • packages/docs/packages/prisma.md
  • packages/memory/README.md
  • packages/parser-expression/README.md
  • packages/parser-mongo/README.md
  • packages/parser-simple/README.md
  • packages/prisma/LICENSE
  • packages/prisma/README.md
  • packages/prisma/package.json
  • packages/prisma/src/adapter/fields.ts
  • packages/prisma/src/adapter/index.ts
  • packages/prisma/src/adapter/module.ts
  • packages/prisma/src/adapter/relations.ts
  • packages/prisma/src/adapter/sort.ts
  • packages/prisma/src/adapter/types.ts
  • packages/prisma/src/adapter/where.ts
  • packages/prisma/src/index.ts
  • packages/prisma/src/metadata/index.ts
  • packages/prisma/src/metadata/module.ts
  • packages/prisma/src/metadata/types.ts
  • packages/prisma/src/provider/constants.ts
  • packages/prisma/src/provider/index.ts
  • packages/prisma/src/provider/module.ts
  • packages/prisma/src/provider/types.ts
  • packages/prisma/test/data/client.ts
  • packages/prisma/test/data/datamodel.ts
  • packages/prisma/test/data/evaluate.ts
  • packages/prisma/test/data/index.ts
  • packages/prisma/test/data/schema.ts
  • packages/prisma/test/data/type.ts
  • packages/prisma/test/prisma/schema.postgres.prisma
  • packages/prisma/test/prisma/schema.prisma
  • packages/prisma/test/unit/acceptance.spec.ts
  • packages/prisma/test/unit/complement.spec.ts
  • packages/prisma/test/unit/engine.db.spec.ts
  • packages/prisma/test/unit/fields.spec.ts
  • packages/prisma/test/unit/filters.spec.ts
  • packages/prisma/test/unit/metadata.spec.ts
  • packages/prisma/test/unit/query.spec.ts
  • packages/prisma/test/unit/relations.spec.ts
  • packages/prisma/test/unit/sort.spec.ts
  • packages/prisma/test/vitest.config.ts
  • packages/prisma/test/vitest.db.config.ts
  • packages/prisma/tsconfig.build.json
  • packages/prisma/tsconfig.json
  • packages/prisma/tsdown.config.ts
  • packages/sql/README.md
  • packages/typeorm/README.md
  • release-please-config.json
📝 Walkthrough

Walkthrough

Adds the @rapiq/prisma package, which serializes parsed queries into Prisma findMany arguments, including filtering, projection, relations, sorting, pagination, metadata, provider handling, tests, CI integration, and documentation.

Changes

Prisma adapter implementation

Layer / File(s) Summary
Package contracts and schema capabilities
packages/prisma/package.json, packages/prisma/src/types, packages/prisma/src/metadata/*, packages/prisma/src/provider/*
Defines the public adapter API, Prisma datamodel metadata resolution, provider capability presets, package exports, and build configuration.
Query serialization pipeline
packages/prisma/src/adapter/*
Translates query fields, relations, filters, sorting, and pagination into Prisma findMany arguments, including null-aware negation, baseline merging, case handling, and adapter state management.
Fixtures and behavioral validation
packages/prisma/test/*, .github/workflows/main.yml
Adds SQLite/PostgreSQL fixtures, unit and acceptance tests, evaluator-based comparisons, engine parity checks, and Prisma database CI execution.
Repository integration and documentation
README.md, packages/docs/*, packages/*/README.md, .agents/*, release-please-*
Documents Prisma support, adds package navigation and installation guidance, updates repository structure references, and registers release metadata.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • tada5hi/rapiq#792: Introduces condition-plan semantics consumed by the Prisma filter interpreter.
  • tada5hi/rapiq#812: Introduces first-class negation semantics implemented by the Prisma adapter.
  • tada5hi/rapiq#762: Relates to shared case-insensitive equality and opt-out behavior.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 36.36% 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.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: introducing the new Prisma adapter package.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/prisma-adapter

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.

@tada5hi
tada5hi force-pushed the feat/prisma-adapter branch from 49329d2 to 9242976 Compare July 26, 2026 10:19

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 8

🧹 Nitpick comments (2)
packages/prisma/package.json (1)

18-22: 🗄️ Data Integrity & Integration | 🔵 Trivial

Confirm Prisma 6.x is the intended test target.

packages/prisma/test/data/client.ts reads generated.Prisma.dmmf.datamodel, which the Prisma 7 prisma-client generator no longer exposes; Prisma 7 would require a different/maintained DMMF source. Documenting why this package still targets Prisma 6.x would make this dependency/version choice clearer.

🤖 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/prisma/package.json` around lines 18 - 22, Document in the Prisma
package metadata why the devDependencies remain pinned to Prisma 6.x:
packages/prisma/test/data/client.ts depends on generated.Prisma.dmmf.datamodel,
which Prisma 7’s prisma-client generator does not expose. Keep the existing
dependency versions unchanged and add a concise explanatory note using the
package’s established metadata/documentation convention.
packages/prisma/test/data/schema.ts (1)

57-63: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider typing createAdapterOptions against the real options type instead of any.

overrides and the return value are untyped (Record<string, any> / as any). Since this helper feeds option overrides (provider, filters.caseSensitive, relations.toMany, etc.) into nearly every spec file in this package, a typo in an override key would silently no-op rather than fail to compile.

♻️ Suggested typing (adjust import path/name to the actual exported options type)
-export function createAdapterOptions(overrides: Record<string, any> = {}) {
+import type { PrismaAdapterOptions } from '../../src';
+
+export function createAdapterOptions(overrides: Partial<PrismaAdapterOptions> = {}) : PrismaAdapterOptions {
     return {
         provider: 'postgresql',
         metadata: defineMetadata(datamodel, 'User'),
         ...overrides,
-    } as any;
+    };
 }

Please confirm the exact exported name/shape of the adapter constructor options type (e.g. in packages/prisma/src/adapter/types.ts) before applying.

🤖 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/prisma/test/data/schema.ts` around lines 57 - 63, Update
createAdapterOptions to use the actual exported adapter constructor options type
for both its overrides parameter and return value, locating and importing the
canonical type from the adapter implementation. Remove Record<string, any> and
the as any cast while preserving the existing defaults and spread behavior so
invalid override keys are caught by TypeScript.
🤖 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/docs/guide/executing-queries.md`:
- Around line 3-12: Update the cross-adapter semantics statement in the guide to
qualify parity as applying to supported features, and reference the documented
Prisma limitations if an existing limitations link is available. Preserve the
adapter overview and its current behavior description.

In `@packages/docs/packages/prisma.md`:
- Around line 171-174: Update the Prisma translation paths for to-many relation
ordering and to-one elemMatch to return typed AdapterErrors with code
FEATURE_UNSUPPORTED instead of emitting Prisma orderBy or some payloads. Locate
the relevant relation-ordering and elemMatch handling symbols, preserve
supported relation behavior, and ensure both unsupported cases follow the
existing adapter error contract before query execution.
- Around line 131-133: Update the PrismaAdapter provider example to include a
constructed metadata object alongside the provider option, matching the required
PrismaAdapterOptions shape and ensuring relation and filter adapters receive
metadata.

In `@packages/prisma/README.md`:
- Around line 98-106: Update the “Case sensitivity” introduction in the Prisma
README to narrow the cross-backend claim or explicitly exempt SQLite equality,
matching the documented SQLite behavior where equals remains case-sensitive.
Keep the provider table and its existing connector-specific details unchanged.

In `@packages/prisma/src/adapter/module.ts`:
- Around line 142-176: The applySelection method must preserve base.select as an
allow-list for both requested root fields and included relations. Intersect
selection.select with base.select, or reject entries outside the base
projection, instead of replacing the base projection; when merging
selection.include into base.select, do not let same-key query selections
overwrite more restrictive base selections. Continue removing conflicting
include/omit arguments after producing the constrained selection.

In `@packages/prisma/test/data/client.ts`:
- Line 136: Replace the `generated.Prisma.dmmf.datamodel` dependency in the
parity fixture with a generated `{ models }` datamodel obtained through a
supported metadata path, such as `@prisma/internals` `getDMMF` or emitted
`--datamodel-json` output. Update the surrounding setup to consume and store
that generated datamodel while preserving the existing parity behavior.

In `@packages/prisma/test/unit/engine.db.spec.ts`:
- Around line 50-107: Extract the duplicated parity records fixture into a
shared module and export it. In packages/prisma/test/unit/engine.db.spec.ts
lines 50-107 and packages/prisma/test/unit/complement.spec.ts lines 44-101,
remove each local records array and import the shared fixture instead,
preserving the existing records symbol and test behavior at both sites.

In `@packages/prisma/test/unit/fields.spec.ts`:
- Around line 79-86: Update the test case around build to avoid the invalid
items.realm metadata path: use a nested relation that exists in the test
datamodel, or revise the fixture and expected select accordingly. Preserve the
intent of verifying that deeper relations remain included for a wholly hydrated
relation without relying on unknown path segments.

---

Nitpick comments:
In `@packages/prisma/package.json`:
- Around line 18-22: Document in the Prisma package metadata why the
devDependencies remain pinned to Prisma 6.x: packages/prisma/test/data/client.ts
depends on generated.Prisma.dmmf.datamodel, which Prisma 7’s prisma-client
generator does not expose. Keep the existing dependency versions unchanged and
add a concise explanatory note using the package’s established
metadata/documentation convention.

In `@packages/prisma/test/data/schema.ts`:
- Around line 57-63: Update createAdapterOptions to use the actual exported
adapter constructor options type for both its overrides parameter and return
value, locating and importing the canonical type from the adapter
implementation. Remove Record<string, any> and the as any cast while preserving
the existing defaults and spread behavior so invalid override keys are caught by
TypeScript.
🪄 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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 89071e5a-65cd-44f3-9dc5-424dd893c0a3

📥 Commits

Reviewing files that changed from the base of the PR and between 997b77a and 9242976.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (62)
  • .agents/architecture.md
  • .agents/structure.md
  • .github/workflows/main.yml
  • .release-please-manifest.json
  • README.md
  • packages/codec-url/README.md
  • packages/core/README.md
  • packages/docs/.vitepress/config.mjs
  • packages/docs/.vitepress/theme/components/PackageLayers.vue
  • packages/docs/guide/executing-queries.md
  • packages/docs/guide/installation.md
  • packages/docs/packages/index.md
  • packages/docs/packages/prisma.md
  • packages/memory/README.md
  • packages/parser-expression/README.md
  • packages/parser-mongo/README.md
  • packages/parser-simple/README.md
  • packages/prisma/LICENSE
  • packages/prisma/README.md
  • packages/prisma/package.json
  • packages/prisma/src/adapter/fields.ts
  • packages/prisma/src/adapter/filters.ts
  • packages/prisma/src/adapter/index.ts
  • packages/prisma/src/adapter/interpreter.ts
  • packages/prisma/src/adapter/module.ts
  • packages/prisma/src/adapter/pagination.ts
  • packages/prisma/src/adapter/relations.ts
  • packages/prisma/src/adapter/sort.ts
  • packages/prisma/src/adapter/types.ts
  • packages/prisma/src/index.ts
  • packages/prisma/src/metadata/index.ts
  • packages/prisma/src/metadata/module.ts
  • packages/prisma/src/metadata/types.ts
  • packages/prisma/src/provider/constants.ts
  • packages/prisma/src/provider/index.ts
  • packages/prisma/src/provider/module.ts
  • packages/prisma/src/provider/types.ts
  • packages/prisma/test/data/client.ts
  • packages/prisma/test/data/datamodel.ts
  • packages/prisma/test/data/evaluate.ts
  • packages/prisma/test/data/index.ts
  • packages/prisma/test/data/schema.ts
  • packages/prisma/test/data/type.ts
  • packages/prisma/test/prisma/schema.postgres.prisma
  • packages/prisma/test/prisma/schema.prisma
  • packages/prisma/test/unit/acceptance.spec.ts
  • packages/prisma/test/unit/complement.spec.ts
  • packages/prisma/test/unit/engine.db.spec.ts
  • packages/prisma/test/unit/fields.spec.ts
  • packages/prisma/test/unit/filters.spec.ts
  • packages/prisma/test/unit/metadata.spec.ts
  • packages/prisma/test/unit/query.spec.ts
  • packages/prisma/test/unit/relations.spec.ts
  • packages/prisma/test/unit/sort.spec.ts
  • packages/prisma/test/vitest.config.ts
  • packages/prisma/test/vitest.db.config.ts
  • packages/prisma/tsconfig.build.json
  • packages/prisma/tsconfig.json
  • packages/prisma/tsdown.config.ts
  • packages/sql/README.md
  • packages/typeorm/README.md
  • release-please-config.json

Comment on lines +3 to +12
A validated `Query` becomes results through an **adapter**. Four ship with rapiq; pick by where your data lives:

| Adapter | Target | Returns |
|---|---|---|
| [@rapiq/typeorm](/packages/typeorm) | TypeORM `SelectQueryBuilder` | mutates the builder in place |
| [@rapiq/prisma](/packages/prisma) | Prisma Client | a `findMany` argument object |
| [@rapiq/sql](/packages/sql) | any SQL driver | parameterized SQL fragments |
| [@rapiq/memory](/packages/memory) | plain objects & arrays | compiled functions / filtered data |

All three consume the same AST, and they agree on semantics: the records a query selects in memory are the records it selects in the database.
All four consume the same AST, and they agree on semantics: the records a query selects in memory are the records it selects in the database.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Qualify the cross-backend parity claim.

Line 12 says all four adapters agree on semantics, but packages/docs/packages/prisma.md documents known exceptions for to-many binding, unsupported relation operations, and SQLite case behavior. Say “for supported features” or link the limitations so users do not assume unconditional Prisma parity.

Suggested wording
-All four consume the same AST, and they agree on semantics: the records a query selects in memory are the records it selects in the database.
+All four consume the same AST and aim for matching semantics for supported features; backend-specific limitations are documented on each package page.
📝 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
A validated `Query` becomes results through an **adapter**. Four ship with rapiq; pick by where your data lives:
| Adapter | Target | Returns |
|---|---|---|
| [@rapiq/typeorm](/packages/typeorm) | TypeORM `SelectQueryBuilder` | mutates the builder in place |
| [@rapiq/prisma](/packages/prisma) | Prisma Client | a `findMany` argument object |
| [@rapiq/sql](/packages/sql) | any SQL driver | parameterized SQL fragments |
| [@rapiq/memory](/packages/memory) | plain objects & arrays | compiled functions / filtered data |
All three consume the same AST, and they agree on semantics: the records a query selects in memory are the records it selects in the database.
All four consume the same AST, and they agree on semantics: the records a query selects in memory are the records it selects in the database.
A validated `Query` becomes results through an **adapter**. Four ship with rapiq; pick by where your data lives:
| Adapter | Target | Returns |
|---|---|---|
| [`@rapiq/typeorm`](/packages/typeorm) | TypeORM `SelectQueryBuilder` | mutates the builder in place |
| [`@rapiq/prisma`](/packages/prisma) | Prisma Client | a `findMany` argument object |
| [`@rapiq/sql`](/packages/sql) | any SQL driver | parameterized SQL fragments |
| [`@rapiq/memory`](/packages/memory) | plain objects & arrays | compiled functions / filtered data |
All four consume the same AST and aim for matching semantics for supported features; backend-specific limitations are documented on each package page.
🤖 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/docs/guide/executing-queries.md` around lines 3 - 12, Update the
cross-adapter semantics statement in the guide to qualify parity as applying to
supported features, and reference the documented Prisma limitations if an
existing limitations link is available. Preserve the adapter overview and its
current behavior description.

Comment thread packages/docs/packages/prisma.md
Comment thread packages/docs/packages/prisma.md
Comment thread packages/prisma/README.md
Comment on lines +142 to +176
/**
* Prisma rejects `select` next to `include` (and next to `omit`) on
* the same level, so exactly one of them survives.
*
* A baseline `select` is a deliberate projection restriction and is
* therefore never dropped: relations the query hydrates join it
* instead: widening a caller-owned projection would expose columns
* the application chose to withhold.
*/
protected applySelection(args: Args, base?: ARGS) : void {
const selection = this.fields.execute();

if (selection.select) {
args.select = selection.select;
delete args.include;
delete (args as Record<string, unknown>).omit;

return;
}

if (!selection.include) {
return;
}

if (base && base.select) {
args.select = { ...base.select, ...selection.include };
delete args.include;
delete (args as Record<string, unknown>).omit;

return;
}

args.include = selection.include;
delete args.select;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== files =="
git ls-files | rg 'packages/prisma/src/adapter/module\.ts|packages/prisma/src'

echo "== target file outline/size =="
wc -l packages/prisma/src/adapter/module.ts
ast-grep outline packages/prisma/src/adapter/module.ts --view compact || true

echo "== relevant lines =="
sed -n '1,240p' packages/prisma/src/adapter/module.ts | nl -ba | sed -n '1,240p'

echo "== search applySelection usages =="
rg -n "applySelection|fields\.execute|applySelection" packages/prisma/src -S

Repository: tada5hi/rapiq

Length of output: 1101


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== target file relevant content =="
sed -n '1,240p' packages/prisma/src/adapter/module.ts > /tmp/module_lines.txt
cat -n /tmp/module_lines.txt | sed -n '1,240p'

echo "== adapter sources outline/content snippets =="
for f in packages/prisma/src/adapter/fields.ts packages/prisma/src/adapter/types.ts packages/prisma/src/adapter/interpreter.ts packages/prisma/src/adapter/filters.ts packages/prisma/src/adapter/relations.ts packages/prisma/src/adapter/module.ts; do
  echo "--- $f ($(wc -l < "$f") lines) ---"
  if fd -p "$f" . >/dev/null 2>&1; then
    ast-grep outline "$f" --view names || true
    rg -n "execute|class|fields|selection|applySelection|omit|include|select" "$f" || true
  else
    echo "MISSING"
  fi
done

echo "== read-only behavioral/static verifier for applySelection =="
python3 - <<'PY'
from pathlib import Path
p = Path('packages/prisma/src/adapter/module.ts')
text = p.read_text()
needles = [
    'if (selection.select) {',
    'args.select = selection.select;',
    'if (base && base.select) {',
    'args.select = { ...base.select, ...selection.include };',
]
for i, n in enumerate(needles, 1):
    print(f"{i}: {n!r} {'FOUND' if n in text else 'MISSING'}")
print("contains delete args.select after include merge:", "delete args.select;" in text[lines:=text.find('if (base && base.select) {') if (lines:=text.find('if (base && base.select) {')) != -1 else 0:])
PY

Repository: tada5hi/rapiq

Length of output: 13376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== FieldAdapter execute/buildSelection source =="
sed -n '80,220p' packages/prisma/src/adapter/fields.ts > /tmp/fields_lines.txt
cat -n /tmp/fields_lines.txt

echo "== fields.ts behavioral static extractor =="
python3 - <<'PY'
from pathlib import Path
p = Path('packages/prisma/src/adapter/fields.ts')
text = p.read_text()
start = text.index('execute() : { select?: Record<string, any>, include?: Record<string, any> }')
print(text[start:start+750])
lines = text.splitlines()
# print line numbers for key implementation lines
for idx, line in enumerate(lines, 1):
    if f'function descend' in line or 'execute() :' in line or 'const select = this.buildSelect' in line or 'if (selected !== false)' in line:
        print(f"{idx}: {line}")
PY

echo "== focused behavioral/static verifier for applySelection outcomes =="
python3 - <<'PY'
from pathlib import Path
module = Path('packages/prisma/src/adapter/module.ts').read_text()

# Model the exact branches used by applySelection; this matches the source's
# order: selection.select -> include-merge-with-base.select -> include-only.
def apply_selection(selection_select, selection_include, base):
    args = {**(base or {})}
    ignore = []
    def delete(key, owner=None):
        if key == 'select':
            args.pop('select', None)
        elif key == 'include':
            args.pop('include', None)
        elif key == 'omit':
            args.pop('omit', None)
        ignore.append(f'{owner or ""}{key}')
    if selection_select:
        args['select'] = selection_select
        delete('include'); delete('omit')
        return args
    if not selection_include:
        return args
    if base and 'select' in base:
        args['select'] = {**base.get('select', {}), **selection_include}
        delete('include'); delete('omit')
        return args
    args['include'] = selection_include
    delete('select')
    return args

base = {'select': {'id': True}}
cases = [
    ("query requests email while base only allows id",
     {'select': {'email': True}}, {'include': None}),
    ("include-only relation under select allowlist",
     {'select': None}, {'include': {'posts': True}}),
    ("shallow include may overwrite nested base select",
     {'select': None}, {'include': {'posts': True}}),
]
for name, sele, include in cases:
    result = apply_selection(sele, include, base)
    print(f"{name}: {result}")
PY

Repository: tada5hi/rapiq

Length of output: 6640


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== static behavioral verifier for applySelection (source-shaped) =="
python3 - <<'PY'
from pathlib import Path
module = Path('packages/prisma/src/adapter/module.ts').read_text()

apply = module[module.index('protected applySelection(args: Args, base?: ARGS) : void {'):module.index('protected buildWhere(base?: Where')].replace('\n', ' ').expandtabs()

checks = {
    'selection.select assigns args.select wholesale': 'if (selection.select) { ... args.select = selection.select; ... }' in apply,
    'selection.select ignores base.select before assignment': '.base.select' not in apply[:apply.index('if (selection.select) { ')+40],
    'selection.include merges into base.select': 'args.select = { ...base.select, ...selection.include };' in apply,
    'selection.include deletes args.select': 'delete args.select;' in apply[apply.index('args.select = { ...base.select, ...selection.include };'):],
    'selection.include deletes args.include': 'delete args.include;' in apply,
    'selection.include deletes omit': 'delete (args as Record<string, unknown>).omit;' in apply,
}
for k, v in checks.items():
    print(f"{k}: {v}")

def apply_selection(select_requested, include_requested, base):
    args = {**(base or {})}
    selection = {'select': select_requested, 'include': include_requested}
    if selection['select']:
        args['select'] = selection['select']
        args.pop('include', None)
        args.pop('omit', None)
        return args
    if not selection['include']:
        return args
    if base and 'select' in base:
        args['select'] = {**base.get('select', {}), **selection['include']}
        args.pop('include', None)
        args.pop('omit', None)
        return args
    args['include'] = selection['include']
    args.pop('select', None)
    return args

cases = [
    ("base only allows id but query selects email",
     {'email': True}, None, {'select': {'id': True}}),
    ("base only allows nested.id but query selects nested.name",
     {'nested': {'name': True}}, None, {'select': {'nested': {'id': True}}}),
    ("include relation under root select allowlist",
     None, {'posts': True}, {'select': {'id': True}}),
    ("spare include relation under root select allowlist",
     None, {'posts': {'id': True, 'author': True}}, {'select': {'id': True}}),
    ("shallow include overwrites nested base select",
     None, {'posts': True}, {'select': {'posts': {'title': True}}}),
]
for name, sel, inc, base in cases:
    out = apply_selection(sel, inc, base)
    base_select = {**(base or {}).pop('select') if base and isinstance(base.get('select'), dict) else {}}
    out_select = out.get('select') or {}
    def has_requested(key, tree=None):
        if tree is None: tree = out_select
        if tree is True: return True
        if key in tree:
            return True if (tree[key] is True) else tree[key] is True or has_requested(key, tree[key])
        if isinstance(tree, dict):
            return any(has_requested(key, v) for v in tree.values())
        return False
    print(f"{name}: out={out}; exposed_requested={'no' if has_requested('no') else 'yes'}")
PY

Repository: tada5hi/rapiq

Length of output: 430


Intersect query selections with the base projection before emitting args.

applySelection replaces args.select with selection.select whenever the query requests fields, so fields outside the caller-owned base.select can be exposed. It should treat base.select as an allow-list for root select entries, or reject out-of-scoped field requests, instead of dropping that restriction. A parallel issue is the include merge: { ...base.select, ...selection.include } can overwrite a more restrictive same-key base selection with a query-selected relation.

🤖 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/prisma/src/adapter/module.ts` around lines 142 - 176, The
applySelection method must preserve base.select as an allow-list for both
requested root fields and included relations. Intersect selection.select with
base.select, or reject entries outside the base projection, instead of replacing
the base projection; when merging selection.include into base.select, do not let
same-key query selections overwrite more restrictive base selections. Continue
removing conflicting include/omit arguments after producing the constrained
selection.


return {
client,
datamodel: generated.Prisma.dmmf.datamodel,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does the "prisma-client-js" generator in Prisma 6.19 still expose Prisma.dmmf reliably?

💡 Result:

In Prisma 6.19, the Prisma.dmmf property is not reliably exposed in the generated client [1][2]. Prisma has been actively moving away from exposing the dmmf object as a public property, as it was originally an undocumented, internal API [3][2][4]. Key points regarding the current status of DMMF in modern Prisma versions: 1. Removal of Prisma.dmmf: The direct exposure of Prisma.dmmf has been intentionally phased out or removed in favor of more stable, documented reflection APIs [1][4]. Users upgrading from older versions to the newer prisma-client generator (which is intended to replace prisma-client-js) often find this property missing [1][5]. 2. Alternative Approaches: - Since Prisma.dmmf is no longer a supported or reliable interface, developers requiring schema metadata are encouraged to use @prisma/internals to generate the DMMF at runtime [1][2]. This involves using functions like getDMMF provided by that package [2][4]. - Prisma has communicated plans to introduce a stable, public-facing alternative, sometimes referred to as Schema Intermediate Representation (SIR), to replace the needs previously met by the undocumented DMMF [4]. 3. Engine Limitations: If you are using the newer engineType = "client" configuration, the DMMF structure—even if accessed via other means—may appear significantly simplified and lack critical metadata (such as relation information, constraints, and specific database types) compared to the default Rust-based engine [6]. Because Prisma.dmmf is not a stable or documented part of the public API, it should not be relied upon for production code [3][2]. Relying on it may lead to breakage in future versions as Prisma continues to optimize its client generation and internal architecture [3][4].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Repo files matching client/data specs:"
git ls-files | rg '^(packages/prisma/test/data/client\.ts|packages/prisma/test/engine\.db\.spec\.ts|prisma\.schema|schema\.prisma|generated/prisma|node\.modules/prisma|package\.json)$' || true

echo
echo "Relevant snippets:"
for f in packages/prisma/test/data/client.ts packages/prisma/test/engine.db.spec.ts; do
  if [ -f "$f" ]; then
    echo "--- $f ($(wc -l < "$f") lines) ---"
    sed -n '1,220p' "$f" | nl -ba | sed -n '1,240p'
  fi
done

echo
echo "Search for generator/provider options and dmmf usage:"
rg -n --glob '!node_modules' --glob '!dist' --glob '!build' 'generator|engineType|provider|dmmf|Prisma\.dmmf|getDMMF|`@prisma/internals`' .

Repository: tada5hi/rapiq

Length of output: 356


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "Tracked candidate files:"
git ls-files | grep -E '(^|/)(package\.json|packages/prisma/test/data/client\.ts|packages/prisma/test/engine\.db\.spec\.ts|schemas?\.prisma|.*schema.*\.prisma)$' || true

echo
echo "packages/prisma/test/data/client.ts:"
sed -n '1,180p' packages/prisma/test/data/client.ts 2>/dev/null || true

echo
echo "packages/prisma/test/engine.db.spec.ts:"
sed -n '1,240p' packages/prisma/test/engine.db.spec.ts 2>/dev/null || true

echo
echo "Search metadata/prisma generator/dmmf usage:"
grep -R -n --exclude-dir=node_modules --exclude-dir=dist --exclude-dir=build \
  -E '(generator|engineType|provider|dmmf|Prisma\.dmmf|getDMMF|`@prisma/internals`|sqlite|postgres|mysql)' . \
  || true

Repository: tada5hi/rapiq

Length of output: 42482


Avoid depending on Prisma.dmmf in this parity suite.

These generators use prisma-client-js, but Prisma treats the DMMF as an internal, non-stable API and the public client property can be missing or reshaped with generator/runtime changes. Generate/store the needed { models } datamodel instead by using a supported Prisma metadata API such as @prisma/internalsgetDMMF, or by emitting the datamodel with --datamodel-json.

🤖 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/prisma/test/data/client.ts` at line 136, Replace the
`generated.Prisma.dmmf.datamodel` dependency in the parity fixture with a
generated `{ models }` datamodel obtained through a supported metadata path,
such as `@prisma/internals` `getDMMF` or emitted `--datamodel-json` output.
Update the surrounding setup to consume and store that generated datamodel while
preserving the existing parity behavior.

Comment on lines +50 to +107
const records : User[] = [
{
id: 1,
first_name: 'Caleb',
last_name: 'Barrows',
email: 'caleb.barrows@gmail.com',
age: 18,
address: 'Hogwarts',
realm_id: 1,
realm: {
id: 1,
name: 'master',
description: null,
},
items: [{
id: 1,
title: 'book',
color: 'red',
}],
},
{
id: 2,
first_name: 'Aston',
last_name: 'Nel',
email: 'ashton.nel@gmail.com',
age: 60,
address: null,
realm_id: null,
realm: null,
items: [],
},
{
id: 3,
first_name: 'Frodo',
last_name: 'Baggins',
email: 'frodo.baggins@gmail.com',
age: 33,
address: 'Mordor',
realm_id: 2,
realm: {
id: 2,
name: 'shire',
description: 'the shire',
},
items: [
{
id: 2,
title: 'ring',
color: null,
},
{
id: 3,
title: 'book',
color: 'blue',
},
],
},
];

Copy link
Copy Markdown

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

Extract the shared parity records fixture instead of duplicating it verbatim. Both files hard-code the identical records array used to assert that Prisma's real engine, @rapiq/memory, and the in-test evaluator all agree — the entire point of this design is that all three compare against the same data, so two independently-maintained copies risk silent drift that would quietly break the parity guarantee without any test failure calling it out.

  • packages/prisma/test/unit/engine.db.spec.ts#L50-L107: move this records array into a shared fixture (e.g. packages/prisma/test/data/records.ts) and import it here.
  • packages/prisma/test/unit/complement.spec.ts#L44-L101: import the same shared fixture instead of redeclaring the array.
📍 Affects 2 files
  • packages/prisma/test/unit/engine.db.spec.ts#L50-L107 (this comment)
  • packages/prisma/test/unit/complement.spec.ts#L44-L101
🤖 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/prisma/test/unit/engine.db.spec.ts` around lines 50 - 107, Extract
the duplicated parity records fixture into a shared module and export it. In
packages/prisma/test/unit/engine.db.spec.ts lines 50-107 and
packages/prisma/test/unit/complement.spec.ts lines 44-101, remove each local
records array and import the shared fixture instead, preserving the existing
records symbol and test behavior at both sites.

Comment thread packages/prisma/test/unit/fields.spec.ts
tada5hi added 4 commits July 26, 2026 13:06
Eliminates group negation from a condition plan by pushing it down to
the leaves: De Morgan over compounds, leaf twins toggled, not(ordering)
rewritten as the dual operator OR a null check (the complement derived
from the semantics table's compare ranges, keeping the table the single
authority), and residual wrappers kept only around mod/size, which have
no complement form.

planCondition retains group negation because sql and memory render it
two-valued cheaply (a CASE wrapper, a `!`). Backends without a
two-valued NOT of their own, prisma first and the other structured-args
ORMs of plan 023 next, consume this transform instead of re-deriving
operator semantics locally.

The transform encodes the settled negation contract: group negation is
the two-valued complement per binding with the quantifier outermost, so
it commutes through elemMatch (not(elemMatch(c)) selects records with
an element failing c, not records without a matching element).
Serializes a parsed Query into a Prisma findMany argument object:
filters to `where`, fields to `select`, relations to `include`, sort to
`orderBy`, pagination to `take`/`skip`. The adapter is stateless and
pure: `execute` maps a value to a value, composition happens before
serialization (mergeQueries, filters.and()), and @prisma/client is
neither a runtime nor a peer dependency.

The `where` is produced by a pipeline of pure passes: planCondition,
core's distributeNegation, quantifier factoring, then a leaf-literal
table. Prisma's three-valued `not`/`NOT` is never relied upon.

Cross-backend semantics hold exactly: relation traversal quantifies
existentially with the quantifier outermost (a left join evaluated per
row), conditions sharing a to-many path within a conjunction are
factored into ONE `some` scope so they bind to the same element as in
sql/typeorm/memory (mixed trees are expanded distributively first,
capped with a typed error), and every quantifier gains its `none: {}`
or absence arm exactly when the interior holds at the all-null binding
an empty collection contributes.

`provider` and `metadata` are required: each metadata fact (relation,
cardinality, nullability, string-typedness) changes what a valid prisma
filter looks like, so a wrong guess is a validation error rather than
degradation.

Parity is measured rather than modelled: the engine suite runs the
matrix through a real prisma client (sqlite by default, postgres under
DB_TYPE=postgres) and cross-checks every condition against
@rapiq/memory and an in-test evaluator, including a mixed-element
record that pins same-element binding and the per-binding complement
of elemMatch.
All of the package's engine-backed specs need a generated prisma client,
so they live behind `test:db` and the default `tests` job stays
codegen-free. The postgres matrix entry is the only place where
`mode: 'insensitive'` exists at all, so the case contract and the
ILIKE-wildcard veto can only be measured there.
Adds the package page (parameter mapping, metadata contract, negation
and same-element semantics, per-connector case behaviour) and the
package README in the layout established by #836, lists the adapter in
the shared "rapiq family" table of every sibling README, and references
it from the overview, installation and executing-queries pages.
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