From bb98529d0df40d72b8cad348fe083faec27114d6 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:19:06 -0700 Subject: [PATCH 1/9] docs(prisma): research the @prisma/client 6 to 7 upgrade Source ledger (prisma/web 42f3146 guide, 7.0.0 to 7.10.1 release notes, 6.19.3 and 7.10.1 tarballs, a probe of both generators) and the classification of every breaking change, before any pack code. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../core/src/packs/prisma-client/RESEARCH.md | 125 ++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 packages/core/src/packs/prisma-client/RESEARCH.md diff --git a/packages/core/src/packs/prisma-client/RESEARCH.md b/packages/core/src/packs/prisma-client/RESEARCH.md new file mode 100644 index 0000000..6a260b4 --- /dev/null +++ b/packages/core/src/packs/prisma-client/RESEARCH.md @@ -0,0 +1,125 @@ +# @prisma/client 6 → 7: evidence and verification limits + +`@prisma/client` and the `prisma` CLI, 6.x → 7.x. Directory `prisma-client` (what `uptide pack new` +names a scoped package, like `sentry-nextjs`). + +## Source ledger + +All read 2026-10-20. Every rule and note in `index.ts` cites one of these. + +| Id | Source | Pin | +|---|---|---| +| G | Upgrade guide, "Upgrade to v7" (`/guides/upgrade-prisma-orm/v7`) | prisma/web `42f314651cb659e885fdf59554196b2ea8efa75d`, `apps/docs/content/docs/guides/upgrade-prisma-orm/v7.mdx` (last changed in `866b345`, 2026-10-12). Reachable with WebFetch at prisma.io the same day; the pinned file is what this pack quotes | +| P | v7 connection pool page (`databases-connections/connection-pool.mdx`), v6 environment variables page (`more/dev-environment/environment-variables.mdx`), v6 config reference (`reference/prisma-config-reference.mdx`), v7 tracing page | same prisma/web commit | +| R0 | Release notes 7.0.0 | prisma/orm tag `7.0.0` (tag object `6843af9`), published 2025-11-19 | +| R | Release notes 7.0.1 … 7.10.1, read for breaking items: only 7.3.0 (mapped enums reverted, #29002) and 7.10.0 (a "matching Prisma 7 CLI" for side-by-side installs) bear on the upgrade | prisma/orm tags `7.3.0` (`cd46c61`), `7.10.0` (`a52d012`), `7.10.1` (`57b7f21`) | +| D6 | Published `@prisma/client` 6.19.3 (`index.d.ts`, `runtime/library.{js,d.ts}`, `package.json`) | npm tarball shasum `b04413143eeb0812a323451bbc1a22767cee11ef` | +| D7 | Published `@prisma/client` 7.10.1 (`index.d.ts`, `runtime/client.d.ts`, `package.json` exports) | npm tarball shasum `c3a6acd14b79ccd788fc93e150df26bae4be5c83` | +| C | Published `prisma` 6.19.3 and 7.10.1 (`build/cli.js`, `build/prisma_schema_build_bg.wasm`, manifests) | shasums `48c80d6a…` (6.19.3), `d4051bf0…` (7.10.1) | +| A | Published `@prisma/adapter-better-sqlite3`, `-neon`, `-d1` 6.19.3 and 7.10.1 declarations | npm tarballs | +| M | npm registry manifests: versions and dependencies of `prisma`, `@prisma/client`, `@prisma/adapter-*`, `@prisma/instrumentation`, `@prisma/client-runtime-utils` | registry, 2026-10-20 | +| X | Probe: a scratch project with `prisma` and `@prisma/client` 7.10.1 generating both `prisma-client-js` and `prisma-client`, compiled with TypeScript 5.9.3; `prisma validate` 7.10.1 on schema variants; `new PrismaClient()` run under Node 22 | not committed; the results are quoted below | + +## How the compiler sees Prisma + +`@prisma/client`'s published types are one line in both majors: `export * from '.prisma/client/default'` +(D6, D7). Every type a consumer compiles against (`PrismaClient`, the `Prisma` namespace, the +constructor options) is **generated** by the `prisma` CLI into `node_modules/.prisma/client` +(`prisma-client-js`) or into the repository (`prisma-client` with `output`). Consequences: + +- The type diff `check` runs on `@prisma/client` has nothing to diff: it reports `no usages found` + for `@prisma/client` in every repository below (BearStudio, jqlang measured with this checkout's + build). The 4403 "removed" exports `packs/queue.json` counts are `runtime/library`, an entry + point user code rarely imports. +- The generated client is from the installed CLI. Linking `@prisma/client` 7 next to a 6-generated + client gives diagnostics no real upgrade has: `packs:truth` reports TS2305 `has no exported member + 'PrismaClient'` at every `import … from '@prisma/client'` (a linked copy no longer reaches + `.prisma/client`), and TS7006/TS7031/TS2339 wherever the generated types fell back to `any` + (bcgov: 130 such diagnostics). A real 7 install runs `prisma generate` at 7, after which none of + them exist. They are never ground truth (see below). +- The compiler-visible 7 changes (TS2554/TS2345 on a constructor without `adapter`, TS2353 on + `datasources`/`datasourceUrl`, TS2339 on `$use`/`$metrics`, TS2694 on `Prisma.Middleware`, all + confirmed by probe X) appear only in a client generated by the 7 CLI. `check` runs no generator, + so this pack finds those sites in source with `detect`, the way the compiler would report them + after `prisma generate`. Engine follow-up (not in this pack): let `check` compile against a client + the target CLI generated in an isolated copy. +- Most of the upgrade is outside TypeScript anyway: `schema.prisma`, `package.json`, the + environment. + +## Release group, companions, new dependencies + +- **`prisma` is a release-group member**, not a companion. Evidence: G, "Update packages": "you + need to update both the `prisma` and `@prisma/client` packages"; R (7.10.0) speaks of "a matching + Prisma 7 CLI"; M: both publish exactly the same version list (6.19.0 … 7.10.1), and neither + depends on the other (`@prisma/client` peers on `prisma: "*"`, optional), so no dependency edge + would move the CLI. Source URL: the pinned guide. +- **Installed driver adapters** (`@prisma/adapter-pg`, `-neon`, `-planetscale`, `-d1`, `-libsql`, + `-better-sqlite3`, `-mariadb`, `-mssql`, `-ppg`) are release-group members too: M shows each + published at every client version and depending on `@prisma/driver-adapter-utils` at its own + exact version; R0 ("Driver Adapter naming updates") renames their classes at 7.0.0, and A confirms + it (6.19.3 exports only `PrismaBetterSQLite3`/`PrismaNeonHTTP`/`PrismaD1HTTP`, 7.10.1 only the new + names). A 6 adapter left next to a 7 client is a combination Prisma never published. Source URL: + the 7.0.0 release notes. This is the weakest companion evidence in the pack (no page says "keep + the adapter at the client's version"): an open question in the pull request. +- **`@prisma/instrumentation` is not a member**: the v7 tracing page installs it `@latest`, and it + depends on neither the client nor the CLI. It stays where it is, and `check` names it. +- **Adapters the upgrade adds are new dependencies**, not companions: an upgrade from 6 without an + adapter must add one (`@prisma/adapter-pg` for PostgreSQL, …) and its driver. Uptide never adds a + dependency (UPT-126: the agent cannot change dependencies), so `client-adapter` is assisted and + its guide says which package the owner adds. Ground truth `with` lists only packages installed + before the upgrade (`prisma`), never an adapter the upgrade added. + +## Classified changes + +"Compiler" = a typed consumer gets a diagnostic once the client is generated at 7 (probe X). +"Detect" = how this pack finds the site. Every rule here is assisted (no `rewrite`); the last +column says why. + +| Change | Source | Compiler | Where | Rule / note (`reported`) | Why assisted | +|---|---|---|---|---|---| +| `prisma` and `@prisma/client` move together | G, M | no | package.json | release group | n/a | +| `new PrismaClient()` needs `adapter` or `accelerateUrl`; `datasources`, `datasourceUrl` removed | G, R0, X | `prisma-client`: yes (TS2554, TS2345 "Property 'adapter' is missing", TS2353). `prisma-client-js`: only `datasources`/`datasourceUrl` (TS2353); `new PrismaClient()` and `{ log }` compile and **throw at construction** (X: "PrismaClient requires a driver adapter…") | TS/JS | `client-adapter` (breaking), detect | the adapter depends on the database provider, is a new dependency, and takes the connection string the client used to read itself | +| Accelerate: `accelerateUrl` instead of an adapter | G, R0 | as above | TS | in `client-adapter`'s guide | same | +| `datasource.url`, `directUrl`, `shadowDatabaseUrl` not allowed in the schema; `prisma.config.ts` carries the URL | G, R0, C (P1012 "is no longer supported in schema files", probe `prisma validate`) | n/a (schema error) | schema.prisma | `schema-datasource-url` (breaking), detect | the URL moves to `prisma.config.ts` (create or edit, `env()` vs `process.env`, `directUrl` becomes `url`) | +| `.env` no longer loaded: by the CLI (6 loaded it unless `prisma.config.ts` existed, P) and by the client at runtime (D6: `runtime/library.js` loads `relativeEnvPaths` in the constructor when no adapter is passed; nothing in D7) | G, R0, P, D6 | no | package.json scripts; schema `env("…")` | `env-not-loaded` (finding + decision, needs a look), detect on scripts that run a database command of the CLI without loading an env file; the decision names the schema's `env()` reads, where the app read the URL on 6 | behavior: whether the environment provides the variable is the owner's knowledge | +| `migrate dev`/`migrate reset` no longer seed; `migrate dev`/`db push` no longer generate; no postinstall generate | G, R0 | no | package.json scripts | `cli-implicit-steps` (finding + decision, needs a look), detect | behavior | +| `--skip-generate`, `--skip-seed`, `db execute --schema/--url`, `migrate diff --from-url/--to-url/--from-schema-datasource/--to-schema-datasource/--shadow-database-url/--from-schema-datamodel/--to-schema-datamodel/--from-local-d1/--to-local-d1`, `generate --data-proxy/--accelerate/--no-engine/--allow-no-models`, `db pull --local-d1`, `prisma introspect`, removed `PRISMA_*` variables | G, R0 | no | package.json scripts | `cli-removed-flags` (breaking), detect | each has its own replacement (config datasource, explicit `prisma generate`); `--skip-generate` alone could be dropped 1:1, but no real site exercises it in a package.json script (basalt's is one), see "Rewrites" | +| `"prisma"` key in package.json (`schema`, `seed`) removed | R0 | no | package.json | `package-json-prisma` (breaking), detect | moves to `prisma.config.ts` (`schema`, `migrations.seed`) | +| `prisma.config.ts` `engine` and `adapter` options removed | R0 | `prisma` config types only | prisma.config.* | `config-removed-options` (breaking), detect | `engine: 'classic'` is simply dropped, but `adapter` moves to the client | +| `prisma-client-js` deprecated for `prisma-client` (generated code in the repository, imports change) | G, R0, C ("Legacy Prisma Client JS generator") | no | schema.prisma | `client-js-generator` (deprecated), detect | changes every import, the output path, ESM output (`moduleFormat`) and some generated APIs (`Prisma.dmmf`, `Prisma.validator`) | +| `output` required | G, R0 | n/a | schema.prisma | **rejected as a 6 → 7 change**: C shows 6.19.3 already refuses `prisma-client` without `output`, and 7.10.1 `prisma-client-js` still generates into `node_modules` (X) | n/a | +| `engineType` | G, R0 | no | schema.prisma | `engine-type` (deprecated), detect: X shows 7.10.1 still accepts `engineType = "binary"` and ignores it | deleting the line is 1:1, but no real site exists; see "Rewrites" | +| ESM (`"type": "module"`, tsconfig `bundler`) | G | no | package.json, tsconfig | **rejected as required**: D7 has no `"type": "module"` and ships CJS `default.js`; `prisma-client` takes `moduleFormat`; X compiled 7 under CommonJS `nodenext`. Named in `client-js-generator`'s guide | n/a | +| Node `^20.19 \|\| ^22.12 \|\| >=24`, TypeScript `>=5.4` | G, M | no | package.json `engines`, `.nvmrc`, `typescript` | `runtime-minimums` (decision) | n/a | +| MongoDB unsupported in 7 | G, R0 | n/a | schema.prisma | `mongodb-unsupported` (breaking), detect | the decision is to stay on 6 | +| `$use` middleware removed | G | yes (TS2339, TS2694 `Prisma.Middleware`) after generation | TS | `client-middleware` (breaking), detect | `$extends` returns a new client: callers must use it | +| Metrics removed (`$metrics`, `previewFeatures = ["metrics"]`) | G, R0, C (P1012 "The preview feature \"metrics\" is not known") | yes after generation | TS, schema.prisma | `metrics-removed` (breaking), detect | no replacement in Prisma; the driver's pool or an extension | +| Driver adapter classes renamed (`PrismaBetterSQLite3`, `PrismaD1HTTP`, `PrismaLibSQL`, `PrismaNeonHTTP`) | R0, A | yes, in the adapter's types | TS | `adapter-renames` (breaking), detect | see "Rewrites" | +| `@prisma/client/runtime/library`, `/wasm`, `/react-native`, `runtime/binary`, `runtime/edge`, `runtime/react-native` removed; `/edge` now means what `/wasm` did | R0 (#28480), D6/D7 `exports` | yes (TS2307) | TS | `removed-entrypoints` (breaking), detect | `runtime/library` → `runtime/client` is 1:1 except for 13 of its 225 exports (D6/D7: `Decimal` internals, `Metric*`, `objectEnumValues`, `warnEnvConflicts`); see "Rewrites" | +| Connection pool and timeouts from the driver; v6 URL parameters (`connection_limit`, `pool_timeout`, `connect_timeout`, `max_idle_connection_lifetime`, `max_connection_lifetime`) not read by the adapters | G, P | no | TS (connection strings), env | `connection-url-params` (finding + decision, needs a look), detect on those parameters in source strings | behavior | +| SSL certificates validated by `pg` (P1010) | G | no | adapter options | `ssl-validation` (decision, PostgreSQL and CockroachDB schemas only) | behavior | +| Mapped enum values: 7.0.0–7.2.x generate the `@map` value; 7.3.0 reverted to 6 | R0, R (7.3.0), G | no | schema.prisma | `mapped-enum-values` (decision, only for a target below 7.3.0 and a schema with a mapped enum member) | behavior | +| Removed client engines, `binaryTargets`, `generator.runtime = "react-native"`, `prisma introspect` | R0 | no | schema.prisma | none (`binaryTargets` is accepted and unused at 7, probe X); `generic` if ever expected | n/a | + +### Rewrites + +None. Each candidate that is 1:1 (dropping `--skip-generate`/`--skip-seed`, dropping +`engineType`, `runtime/library` → `runtime/client` when no missing export is imported, an import +of a renamed adapter class) has at most one real site in the ground truth below, and a rewrite with no +real site runs assisted anyway (UPT-48, `unexercised`). So the code would ship and never run. Add +the rewrite together with the first ground-truth repository that exercises it. + +## Detection scope + +- TypeScript/JavaScript: every file of the workspace's TypeScript project (`projectFiles`, as the + redis pack reads wrapper modules) plus importing files, because `check` finds no usages of + `@prisma/client` (above). A file is read only when its text names `PrismaClient`, `$use`, + `$metrics`, a Prisma adapter or a Prisma entry point. `new PrismaClient(...)` counts only where + `PrismaClient` is imported or required from `@prisma/client` or from a module path that ends in + `/client` or names `prisma`/`generated` (a generated client); a local `PrismaClient` (a jest mock) + is never a site (fixture `client-adapter` line 9). +- `schema.prisma` and any `*.prisma` file of a multi-file schema, `package.json`, `prisma.config.*`, + `.nvmrc`/`.node-version`: the pack's inventory. +- Fixtures for package.json and schema files follow sentry-node and the typescript pack: the file + named by `filename.txt`, with `// @uptide` markers (comments in a Prisma schema; ignored by the + line-based package.json detectors). From 206961fbadfbdde530bbadfb462768e267412094 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:19:12 -0700 Subject: [PATCH 2/9] feat(prisma): a candidate @prisma/client 6 to 7 pack Detect-based rules: check sees no usage of @prisma/client (its types are the generated client), so the pack reads schema.prisma, package.json, prisma.config.* and the workspace's TypeScript project. One rewrite (cli-skip-flags: 7 never generates or seeds where the flags skipped it); everything else is assisted. prisma and installed driver adapters are a release group. Scaffolded with uptide pack new. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../core/src/packs/prisma-client/detect.ts | 518 ++++++++++++++++++ .../fixtures/client-adapter-mock/after.ts | 3 + .../fixtures/client-adapter-mock/before.ts | 3 + .../fixtures/client-adapter/after.ts | 25 + .../fixtures/client-adapter/before.ts | 25 + .../fixtures/client-api/after.ts | 20 + .../fixtures/client-api/before.ts | 20 + .../prisma-client/fixtures/config/after.ts | 10 + .../prisma-client/fixtures/config/before.ts | 10 + .../fixtures/config/filename.txt | 1 + .../fixtures/schema-mongodb/after.ts | 8 + .../fixtures/schema-mongodb/before.ts | 8 + .../fixtures/schema-mongodb/filename.txt | 1 + .../prisma-client/fixtures/schema/after.ts | 23 + .../prisma-client/fixtures/schema/before.ts | 23 + .../fixtures/schema/filename.txt | 1 + .../prisma-client/fixtures/scripts/after.ts | 30 + .../prisma-client/fixtures/scripts/before.ts | 30 + .../fixtures/scripts/filename.txt | 1 + .../src/packs/prisma-client/ground-truth.json | 5 + .../core/src/packs/prisma-client/index.ts | 384 +++++++++++++ .../packs/prisma-client/prisma-client.test.ts | 106 ++++ .../src/packs/prisma-client/verification.json | 32 ++ packages/core/src/packs/registry.ts | 7 + 24 files changed, 1294 insertions(+) create mode 100644 packages/core/src/packs/prisma-client/detect.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-adapter/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-adapter/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-api/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/client-api/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/config/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/config/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/config/filename.txt create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema-mongodb/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema-mongodb/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema-mongodb/filename.txt create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/schema/filename.txt create mode 100644 packages/core/src/packs/prisma-client/fixtures/scripts/after.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/scripts/before.ts create mode 100644 packages/core/src/packs/prisma-client/fixtures/scripts/filename.txt create mode 100644 packages/core/src/packs/prisma-client/ground-truth.json create mode 100644 packages/core/src/packs/prisma-client/index.ts create mode 100644 packages/core/src/packs/prisma-client/prisma-client.test.ts create mode 100644 packages/core/src/packs/prisma-client/verification.json diff --git a/packages/core/src/packs/prisma-client/detect.ts b/packages/core/src/packs/prisma-client/detect.ts new file mode 100644 index 0000000..03dc050 --- /dev/null +++ b/packages/core/src/packs/prisma-client/detect.ts @@ -0,0 +1,518 @@ +import { ts } from 'ts-morph'; +import type { Finding } from '../../domain/report.js'; +import type { SourceSite } from '../contract.js'; +import type { TransformResult } from '../types.js'; + +/** + * Where the Prisma 7 changes live. `check` sees no usage of `@prisma/client` (its types are the + * generated client: RESEARCH.md), so every site is found here, in a Prisma schema, a + * package.json, a Prisma config, or a source file that constructs or calls the client. + */ + +const SOURCE = /\.[cm]?[jt]sx?$/; +const SCHEMA = /\.prisma$/; +const MANIFEST = /(?:^|\/)package\.json$/; +export const CONFIG = /(?:^|\/)prisma\.config\.[cm]?[jt]s$/; + +const site = (line: number, column: number, snippet: string, name?: string): SourceSite => ({ + line, + column, + snippet: snippet.trim(), + ...(name ? { name } : {}), +}); + +// --- schema.prisma --------------------------------------------------------------------- + +interface SchemaProperty { + block: 'datasource' | 'generator'; + key: string; + value: string; + line: number; + column: number; + text: string; +} + +/** The properties of each `datasource` and `generator` block, line by line, comments dropped. */ +export function schemaProperties(text: string): SchemaProperty[] { + const out: SchemaProperty[] = []; + let block: SchemaProperty['block'] | undefined; + text.split('\n').forEach((raw, i) => { + const line = raw.replace(/\s*\/\/.*$/, ''); + const open = /^\s*(datasource|generator)\s+\w+\s*\{\s*$/.exec(line); + if (open) block = open[1] as SchemaProperty['block']; + else if (block && /^\s*\}/.test(line)) block = undefined; + else if (block) { + const prop = /^(\s*)(\w+)\s*=\s*(.+?)\s*$/.exec(line); + if (prop) + out.push({ + block, + key: prop[2] as string, + value: prop[3] as string, + line: i + 1, + column: (prop[1] ?? '').length + 1, + text: raw, + }); + } + }); + return out; +} + +function schemaSites(test: (p: SchemaProperty) => boolean) { + return (text: string, file: string): SourceSite[] => + SCHEMA.test(file) + ? schemaProperties(text) + .filter(test) + .map((p) => site(p.line, p.column, p.text, p.key)) + : []; +} + +export const datasourceUrlSites = schemaSites( + (p) => p.block === 'datasource' && /^(?:url|directUrl|shadowDatabaseUrl)$/.test(p.key), +); +export const mongodbSites = schemaSites( + (p) => p.block === 'datasource' && p.key === 'provider' && p.value === '"mongodb"', +); +export const clientJsSites = schemaSites( + (p) => p.block === 'generator' && p.key === 'provider' && p.value === '"prisma-client-js"', +); +export const engineTypeSites = schemaSites( + (p) => p.block === 'generator' && p.key === 'engineType', +); +const metricsPreviewSites = schemaSites( + (p) => p.block === 'generator' && p.key === 'previewFeatures' && /"metrics"/.test(p.value), +); + +/** `env("NAME")` reads in a datasource: where the app read its URL on 6. */ +export function schemaEnvReads(text: string): { line: number; name: string }[] { + return schemaProperties(text) + .filter((p) => p.block === 'datasource') + .flatMap((p) => { + const env = /env\(\s*"([^"]+)"\s*\)/.exec(p.value); + return env ? [{ line: p.line, name: env[1] as string }] : []; + }); +} + +/** Enum members with `@map`: their runtime value changed in 7.0.0–7.2.x only. */ +export function mappedEnumMembers(text: string): number[] { + const lines: number[] = []; + let inEnum = false; + text.split('\n').forEach((raw, i) => { + const line = raw.replace(/\s*\/\/.*$/, ''); + if (/^\s*enum\s+\w+\s*\{/.test(line)) inEnum = true; + else if (inEnum && /^\s*\}/.test(line)) inEnum = false; + else if (inEnum && /^\s*\w+\s+@map\(/.test(line)) lines.push(i + 1); + }); + return lines; +} + +// --- package.json ---------------------------------------------------------------------- + +interface Script { + line: number; + column: number; + name: string; + value: string; + text: string; +} + +/** Each script of the top-level `scripts` object, on the line that declares it. */ +export function scripts(text: string): Script[] { + const lines = text.split('\n'); + const open = lines.findIndex( + (line, i) => /^\s*"scripts"\s*:\s*\{/.test(line) && depthAt(lines, i) === 1, + ); + if (open < 0) return []; + const out: Script[] = []; + for (let i = open + 1; i < lines.length; i++) { + const line = lines[i] ?? ''; + if (/^\s*\}/.test(line)) break; + const m = /^(\s*)"([^"]+)"\s*:\s*"((?:[^"\\]|\\.)*)"/.exec(line); + if (m) + out.push({ + line: i + 1, + column: (m[1] ?? '').length + 1, + name: m[2] as string, + value: m[3] as string, + text: line, + }); + } + return out; +} + +/** JSON object depth at the start of line `index` (strings skipped). */ +function depthAt(lines: string[], index: number): number { + let depth = 0; + for (const line of lines.slice(0, index)) + for (const token of line.replace(/"(?:[^"\\]|\\.)*"/g, '""').matchAll(/[{}]/g)) + depth += token[0] === '{' ? 1 : -1; + return depth; +} + +/** The `prisma …` commands of a script, each as its arguments (`db push --skip-generate`). */ +export function prismaCommands(value: string): string[] { + return value.split(/&&|\|\||;|\|/).flatMap((part) => { + const m = + /(?:^|\s)(?:(?:npx|bunx|pnpx)\s+(?:--?\S+\s+)*|(?:pnpm|yarn|bun)\s+(?:exec\s+|dlx\s+|x\s+)?|\S*node_modules\/\.bin\/)?prisma\s+(.*)$/.exec( + part.trim(), + ); + return m ? [(m[1] ?? '').trim()] : []; + }); +} + +/** A database command of the CLI: it needs the datasource URL. */ +const DB_COMMAND = + /^(?:migrate\s+(?:dev|deploy|reset|status|resolve|diff)|db\s+(?:push|pull|seed|execute)|studio)\b/; +/** Loads an env file before the command: dotenv-cli, Node's flag, env-cmd, Bun. */ +const LOADS_ENV = /\bdotenv(?:x)?\b|--env-file|\benv-cmd\b|(?:^|\s)bunx?\s/; + +export function envNotLoadedSites(text: string, file: string): SourceSite[] { + if (!MANIFEST.test(file)) return []; + return scripts(text) + .filter( + (s) => !LOADS_ENV.test(s.value) && prismaCommands(s.value).some((c) => DB_COMMAND.test(c)), + ) + .map((s) => site(s.line, s.column, s.text, s.name)); +} + +/** `migrate dev`/`migrate reset` without `--skip-seed`, `migrate dev`/`db push` without `--skip-generate`. */ +export function implicitStepSites(text: string, file: string): SourceSite[] { + if (!MANIFEST.test(file)) return []; + const implicit = (command: string) => + (/^migrate\s+(?:dev|reset)\b/.test(command) && !/--skip-seed\b/.test(command)) || + (/^(?:migrate\s+dev|db\s+push)\b/.test(command) && !/--skip-generate\b/.test(command)); + return scripts(text) + .filter((s) => { + const commands = prismaCommands(s.value); + // A script that runs `prisma generate` itself after the command does not rely on it. + return commands.some(implicit) && !commands.some((c, i) => i > 0 && /^generate\b/.test(c)); + }) + .map((s) => site(s.line, s.column, s.text, s.name)); +} + +const SKIP_FLAG = /--skip-(?:generate|seed)\b/; + +export function skipFlagSites(text: string, file: string): SourceSite[] { + if (!MANIFEST.test(file)) return []; + return scripts(text) + .filter((s) => + prismaCommands(s.value).some( + (c) => /^(?:migrate\s+dev|db\s+push)\b/.test(c) && SKIP_FLAG.test(c), + ), + ) + .map((s) => site(s.line, s.column, s.text, s.name)); +} + +/** + * 7 neither generates after `migrate dev`/`db push` nor seeds after `migrate dev`, which is what + * the flags turned off: without them the command does what it did. Only the flags go. + */ +export function rewriteSkipFlags(text: string, finding: Pick): TransformResult { + const script = scripts(text).find((s) => s.line === finding.usage.line); + if (!script) return { text, applied: false, reason: 'the reported line is not a script' }; + // Every skip flag of the script must belong to `migrate dev` or `db push`. + const count = (s: string) => s.match(/--skip-(?:generate|seed)\b/g)?.length ?? 0; + const owned = prismaCommands(script.value) + .filter((c) => /^(?:migrate\s+dev|db\s+push)\b/.test(c)) + .reduce((n, c) => n + count(c), 0); + if (owned === 0 || owned !== count(script.value)) + return { text, applied: false, reason: 'a skip flag on another command: drop it by hand' }; + const lines = text.split('\n'); + const index = finding.usage.line - 1; + const value = script.value.replace(/\s--skip-(?:generate|seed)\b/g, ''); + lines[index] = (lines[index] ?? '').replace(`"${script.value}"`, `"${value}"`); + return { + text: lines.join('\n'), + applied: true, + reason: '`--skip-generate` and `--skip-seed` are gone: 7 never generates or seeds there', + }; +} + +const REMOVED_FLAGS = + /^(?:db\s+execute\b.*--(?:schema|url)\b|migrate\s+diff\b.*--(?:(?:from|to)-(?:url|schema-datasource|schema-datamodel|local-d1)|shadow-database-url)\b|generate\b.*--(?:data-proxy|accelerate|no-engine|allow-no-models)\b|db\s+pull\b.*--(?:local-d1|url)\b|introspect\b)/; +const REMOVED_ENV = + /\bPRISMA_(?:CLI_QUERY_ENGINE_TYPE|CLIENT_ENGINE_TYPE|QUERY_ENGINE_BINARY|QUERY_ENGINE_LIBRARY|GENERATE_SKIP_AUTOINSTALL|SKIP_POSTINSTALL_GENERATE|GENERATE_IN_POSTINSTALL|GENERATE_DATAPROXY|GENERATE_NO_ENGINE|CLIENT_NO_RETRY|MIGRATE_SKIP_GENERATE|MIGRATE_SKIP_SEED)=/; + +export function removedFlagSites(text: string, file: string): SourceSite[] { + if (!MANIFEST.test(file)) return []; + return scripts(text) + .filter( + (s) => + REMOVED_ENV.test(s.value) || prismaCommands(s.value).some((c) => REMOVED_FLAGS.test(c)), + ) + .map((s) => site(s.line, s.column, s.text, s.name)); +} + +/** The top-level `"prisma"` object (`schema`, `seed`): 7 ignores it. */ +export function packageJsonPrismaSites(text: string, file: string): SourceSite[] { + if (!MANIFEST.test(file)) return []; + const lines = text.split('\n'); + return lines.flatMap((line, i) => + /^\s*"prisma"\s*:\s*\{/.test(line) && depthAt(lines, i) === 1 + ? [site(i + 1, line.search(/\S/) + 1, line, 'prisma')] + : [], + ); +} + +// --- TypeScript and JavaScript --------------------------------------------------------- + +function parse(text: string, file: string): ts.SourceFile { + const kind = /\.[cm]?jsx?$/.test(file) + ? ts.ScriptKind.JSX + : /\.tsx$/.test(file) + ? ts.ScriptKind.TSX + : ts.ScriptKind.TS; + return ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true, kind); +} + +function nodeSite(node: ts.Node, name?: string): SourceSite { + const source = node.getSourceFile(); + const { line, character } = source.getLineAndCharacterOfPosition(node.getStart(source)); + return site(line + 1, character + 1, source.text.split('\n')[line] ?? '', name); +} + +function walk(node: ts.Node, visit: (node: ts.Node) => void): void { + visit(node); + node.forEachChild((child) => walk(child, visit)); +} + +function unwrap(node: ts.Expression): ts.Expression { + let e = node; + while ( + ts.isParenthesizedExpression(e) || + ts.isAsExpression(e) || + ts.isAwaitExpression(e) || + ts.isNonNullExpression(e) || + ts.isSatisfiesExpression(e) + ) + e = e.expression; + return e; +} + +/** A module a Prisma client comes from: `@prisma/client`, a generated client, a local re-export. */ +const CLIENT_MODULE = /prisma|generated|(?:^|\/)client(?:\/index)?(?:\.[cm]?[jt]s)?$/i; + +/** The string a module specifier is: a literal, or a variable of this file initialized with one. */ +function specifierText(source: ts.SourceFile, node: ts.Expression | undefined): string | undefined { + if (!node) return undefined; + if (ts.isStringLiteralLike(node)) return node.text; + if (!ts.isIdentifier(node)) return undefined; + let found: string | undefined; + walk(source, (n) => { + if ( + ts.isVariableDeclaration(n) && + ts.isIdentifier(n.name) && + n.name.text === node.text && + n.initializer && + ts.isStringLiteralLike(n.initializer) + ) + found = n.initializer.text; + }); + return found; +} + +/** `require(x)` or `import(x)`, through `await`, parentheses and casts. */ +function loadedModule(source: ts.SourceFile, node: ts.Expression): string | undefined { + const e = unwrap(node); + if (!ts.isCallExpression(e)) return undefined; + const isRequire = ts.isIdentifier(e.expression) && e.expression.text === 'require'; + const isImport = e.expression.kind === ts.SyntaxKind.ImportKeyword; + return isRequire || isImport ? specifierText(source, e.arguments[0]) : undefined; +} + +/** Local names bound to a Prisma `PrismaClient` class: imported, required or dynamically imported. */ +function clientNames(source: ts.SourceFile): Set { + const names = new Set(); + walk(source, (node) => { + if ( + ts.isImportDeclaration(node) && + ts.isStringLiteral(node.moduleSpecifier) && + CLIENT_MODULE.test(node.moduleSpecifier.text) + ) { + const bindings = node.importClause?.namedBindings; + if (bindings && ts.isNamedImports(bindings)) + for (const el of bindings.elements) + if ((el.propertyName ?? el.name).text === 'PrismaClient') names.add(el.name.text); + } + if ( + ts.isVariableDeclaration(node) && + node.initializer && + ts.isObjectBindingPattern(node.name) + ) { + const from = loadedModule(source, node.initializer); + if (from === undefined || !CLIENT_MODULE.test(from)) return; + for (const el of node.name.elements) { + const imported = el.propertyName ?? el.name; + if ( + ts.isIdentifier(imported) && + imported.text === 'PrismaClient' && + ts.isIdentifier(el.name) + ) + names.add(el.name.text); + } + } + }); + return names; +} + +const propertyName = (p: ts.ObjectLiteralElementLike): string | undefined => + p.name && (ts.isIdentifier(p.name) || ts.isStringLiteral(p.name)) ? p.name.text : undefined; + +/** + * `new PrismaClient(…)` that 7 rejects: no options, or options with neither `adapter` nor + * `accelerateUrl`. The site is a `datasources`/`datasourceUrl` property when there is one (what + * the adapter replaces), else the `new`. Options the file builds elsewhere, or spreads, are not + * read: those are not sites. + */ +export function clientAdapterSites(text: string, file: string): SourceSite[] { + if (!SOURCE.test(file) || !text.includes('PrismaClient')) return []; + const source = parse(text, file); + const names = clientNames(source); + if (names.size === 0) return []; + const sites: SourceSite[] = []; + walk(source, (node) => { + if ( + !ts.isNewExpression(node) || + !ts.isIdentifier(node.expression) || + !names.has(node.expression.text) + ) + return; + const options = node.arguments?.[0]; + if (!options) { + sites.push(nodeSite(node, 'PrismaClient')); + return; + } + if (!ts.isObjectLiteralExpression(options)) return; + if (options.properties.some((p) => ts.isSpreadAssignment(p))) return; + const keys = options.properties.map(propertyName); + if (keys.includes('adapter') || keys.includes('accelerateUrl')) return; + const datasource = options.properties.find((p) => + ['datasources', 'datasourceUrl'].includes(propertyName(p) ?? ''), + ); + sites.push( + datasource ? nodeSite(datasource, propertyName(datasource)) : nodeSite(node, 'PrismaClient'), + ); + }); + return sites; +} + +/** `client.$use((params, next) => …)`: the removed middleware API, and `Prisma.Middleware`. */ +export function middlewareSites(text: string, file: string): SourceSite[] { + if (!SOURCE.test(file) || !/\$use\b|\bMiddleware\b/.test(text)) return []; + const sites: SourceSite[] = []; + walk(parse(text, file), (node) => { + if ( + ts.isCallExpression(node) && + ts.isPropertyAccessExpression(node.expression) && + node.expression.name.text === '$use' && + node.arguments.length === 1 + ) + sites.push(nodeSite(node, '$use')); + if ( + ts.isQualifiedName(node) && + ts.isIdentifier(node.left) && + node.left.text === 'Prisma' && + node.right.text === 'Middleware' + ) + sites.push(nodeSite(node, 'Prisma.Middleware')); + }); + return sites; +} + +/** `client.$metrics` and `previewFeatures = ["metrics"]`. */ +export function metricsSites(text: string, file: string): SourceSite[] { + if (SCHEMA.test(file)) return metricsPreviewSites(text, file); + if (!SOURCE.test(file) || !text.includes('$metrics')) return []; + const sites: SourceSite[] = []; + walk(parse(text, file), (node) => { + if (ts.isPropertyAccessExpression(node) && node.name.text === '$metrics') + sites.push(nodeSite(node, '$metrics')); + }); + return sites; +} + +/** Entry points `@prisma/client` 7 no longer exports (its `package.json` `exports`). */ +export const REMOVED_ENTRYPOINT = + /^@prisma\/client\/(?:wasm|react-native|generator-build|runtime\/(?:library|binary|edge|react-native|wasm-engine-edge))(?:\.js)?$/; + +export function removedEntrypointSites(text: string, file: string): SourceSite[] { + if (!SOURCE.test(file) || !text.includes('@prisma/client/')) return []; + const source = parse(text, file); + const sites: SourceSite[] = []; + walk(source, (node) => { + let specifier: string | undefined; + if ((ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier) + specifier = ts.isStringLiteral(node.moduleSpecifier) ? node.moduleSpecifier.text : undefined; + else if (ts.isCallExpression(node)) specifier = loadedModule(source, node); + else if ( + ts.isImportTypeNode(node) && + ts.isLiteralTypeNode(node.argument) && + ts.isStringLiteral(node.argument.literal) + ) + specifier = node.argument.literal.text; + if (specifier !== undefined && REMOVED_ENTRYPOINT.test(specifier)) + sites.push(nodeSite(node, specifier)); + }); + return sites; +} + +/** Adapter classes 7.0.0 renamed (release notes); 6.19 exports only the old names. */ +export const ADAPTER_RENAMES: Record = { + PrismaBetterSQLite3: 'PrismaBetterSqlite3', + PrismaD1HTTP: 'PrismaD1Http', + PrismaLibSQL: 'PrismaLibSql', + PrismaNeonHTTP: 'PrismaNeonHttp', +}; + +export function adapterRenameSites(text: string, file: string): SourceSite[] { + if (!SOURCE.test(file) || !text.includes('@prisma/adapter-')) return []; + const sites: SourceSite[] = []; + walk(parse(text, file), (node) => { + if ( + !ts.isImportDeclaration(node) || + !ts.isStringLiteral(node.moduleSpecifier) || + !node.moduleSpecifier.text.startsWith('@prisma/adapter-') + ) + return; + const bindings = node.importClause?.namedBindings; + if (bindings && ts.isNamedImports(bindings)) + for (const el of bindings.elements) { + const name = (el.propertyName ?? el.name).text; + if (ADAPTER_RENAMES[name]) sites.push(nodeSite(el, name)); + } + }); + return sites; +} + +/** v6 connection URL parameters the v7 driver adapters do not read (connection pool page). */ +const URL_PARAM = + /[?&](?:connection_limit|pool_timeout|connect_timeout|max_idle_connection_lifetime|max_connection_lifetime)=/; + +export function urlParamSites(text: string, file: string): SourceSite[] { + if (!SOURCE.test(file) || !URL_PARAM.test(text)) return []; + return text.split('\n').flatMap((line, i) => { + const at = line.search(URL_PARAM); + return at >= 0 && !/^\s*(?:\/\/|\*)/.test(line) ? [site(i + 1, at + 1, line)] : []; + }); +} + +/** `engine` and `adapter` in a Prisma config: removed in 7. */ +export function configOptionSites(text: string, file: string): SourceSite[] { + if (!CONFIG.test(file)) return []; + const sites: SourceSite[] = []; + walk(parse(text, file), (node) => { + const config = + ts.isCallExpression(node) && + ts.isIdentifier(node.expression) && + node.expression.text === 'defineConfig' + ? node.arguments[0] + : ts.isExportAssignment(node) + ? unwrap(node.expression) + : undefined; + if (config && ts.isObjectLiteralExpression(config)) + for (const p of config.properties) { + const name = propertyName(p); + if (name === 'engine' || name === 'adapter') sites.push(nodeSite(p, name)); + } + }); + return sites; +} diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts new file mode 100644 index 0000000..50762ad --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts @@ -0,0 +1,3 @@ +// A jest mock of the client: its PrismaClient is a local function, not Prisma's. +export const PrismaClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +export const mockPrisma = new PrismaClient(); // @uptide client-adapter keep diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts new file mode 100644 index 0000000..50762ad --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts @@ -0,0 +1,3 @@ +// A jest mock of the client: its PrismaClient is a local function, not Prisma's. +export const PrismaClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +export const mockPrisma = new PrismaClient(); // @uptide client-adapter keep diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter/after.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter/after.ts new file mode 100644 index 0000000..205f6c5 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter/after.ts @@ -0,0 +1,25 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '@prisma/client'; + +export const bare = new PrismaClient(); // @uptide client-adapter +export const logged = new PrismaClient({ log: ['query'] }); // @uptide client-adapter +export const routed = new PrismaClient({ + datasources: { db: { url: process.env.DATABASE_URL } }, // @uptide client-adapter +}); +export const direct = new PrismaClient({ + log: ['warn'], + datasourceUrl: process.env.DATABASE_URL, // @uptide client-adapter +}); +export const adapted = new PrismaClient({ adapter: new PrismaPg({ connectionString: '' }) }); // @uptide client-adapter keep +export const accelerated = new PrismaClient({ accelerateUrl: process.env.ACCELERATE_URL }); // @uptide client-adapter keep +export const built = new PrismaClient(options); // @uptide client-adapter keep +export const spread = new PrismaClient({ ...options }); // @uptide client-adapter keep + +const { PrismaClient: Required } = require('@prisma/client'); +export const required = new Required(); // @uptide client-adapter + +export async function generated() { + const clientModule: string = '../generated/client/index.js'; + const { PrismaClient: Generated } = (await import(clientModule)) as { PrismaClient: new () => unknown }; + return new Generated(); // @uptide client-adapter +} diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter/before.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter/before.ts new file mode 100644 index 0000000..205f6c5 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter/before.ts @@ -0,0 +1,25 @@ +import { PrismaPg } from '@prisma/adapter-pg'; +import { PrismaClient } from '@prisma/client'; + +export const bare = new PrismaClient(); // @uptide client-adapter +export const logged = new PrismaClient({ log: ['query'] }); // @uptide client-adapter +export const routed = new PrismaClient({ + datasources: { db: { url: process.env.DATABASE_URL } }, // @uptide client-adapter +}); +export const direct = new PrismaClient({ + log: ['warn'], + datasourceUrl: process.env.DATABASE_URL, // @uptide client-adapter +}); +export const adapted = new PrismaClient({ adapter: new PrismaPg({ connectionString: '' }) }); // @uptide client-adapter keep +export const accelerated = new PrismaClient({ accelerateUrl: process.env.ACCELERATE_URL }); // @uptide client-adapter keep +export const built = new PrismaClient(options); // @uptide client-adapter keep +export const spread = new PrismaClient({ ...options }); // @uptide client-adapter keep + +const { PrismaClient: Required } = require('@prisma/client'); +export const required = new Required(); // @uptide client-adapter + +export async function generated() { + const clientModule: string = '../generated/client/index.js'; + const { PrismaClient: Generated } = (await import(clientModule)) as { PrismaClient: new () => unknown }; + return new Generated(); // @uptide client-adapter +} diff --git a/packages/core/src/packs/prisma-client/fixtures/client-api/after.ts b/packages/core/src/packs/prisma-client/fixtures/client-api/after.ts new file mode 100644 index 0000000..6bf981f --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-api/after.ts @@ -0,0 +1,20 @@ +import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'; // @uptide adapter-renames +import { PrismaNeon } from '@prisma/adapter-neon'; // @uptide adapter-renames keep +import { Prisma, PrismaClient } from '@prisma/client'; +import type { Decimal } from '@prisma/client/runtime/library'; // @uptide removed-entrypoints +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/client'; // @uptide removed-entrypoints keep + +const prisma = new PrismaClient({ adapter: new PrismaBetterSQLite3({ url: 'file:./dev.db' }) }); // @uptide client-adapter keep + +prisma.$use(async (params, next) => next(params)); // @uptide client-middleware +const timing: Prisma.Middleware = async (params, next) => next(params); // @uptide client-middleware +app.use(express.json()); // @uptide client-middleware keep + +export const metrics = await prisma.$metrics.json(); // @uptide metrics-removed +export const counts = report.metrics.json(); // @uptide metrics-removed keep + +export const pooled = `postgresql://u:p@db:5432/app?connection_limit=5&pool_timeout=10`; // @uptide connection-url-params +export const scoped = 'postgresql://u:p@db:5432/app?schema=public'; // @uptide connection-url-params keep + +export const edge = await import('@prisma/client/wasm'); // @uptide removed-entrypoints +export { timing, PrismaNeon, PrismaClientKnownRequestError, type Decimal }; diff --git a/packages/core/src/packs/prisma-client/fixtures/client-api/before.ts b/packages/core/src/packs/prisma-client/fixtures/client-api/before.ts new file mode 100644 index 0000000..6bf981f --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/client-api/before.ts @@ -0,0 +1,20 @@ +import { PrismaBetterSQLite3 } from '@prisma/adapter-better-sqlite3'; // @uptide adapter-renames +import { PrismaNeon } from '@prisma/adapter-neon'; // @uptide adapter-renames keep +import { Prisma, PrismaClient } from '@prisma/client'; +import type { Decimal } from '@prisma/client/runtime/library'; // @uptide removed-entrypoints +import { PrismaClientKnownRequestError } from '@prisma/client/runtime/client'; // @uptide removed-entrypoints keep + +const prisma = new PrismaClient({ adapter: new PrismaBetterSQLite3({ url: 'file:./dev.db' }) }); // @uptide client-adapter keep + +prisma.$use(async (params, next) => next(params)); // @uptide client-middleware +const timing: Prisma.Middleware = async (params, next) => next(params); // @uptide client-middleware +app.use(express.json()); // @uptide client-middleware keep + +export const metrics = await prisma.$metrics.json(); // @uptide metrics-removed +export const counts = report.metrics.json(); // @uptide metrics-removed keep + +export const pooled = `postgresql://u:p@db:5432/app?connection_limit=5&pool_timeout=10`; // @uptide connection-url-params +export const scoped = 'postgresql://u:p@db:5432/app?schema=public'; // @uptide connection-url-params keep + +export const edge = await import('@prisma/client/wasm'); // @uptide removed-entrypoints +export { timing, PrismaNeon, PrismaClientKnownRequestError, type Decimal }; diff --git a/packages/core/src/packs/prisma-client/fixtures/config/after.ts b/packages/core/src/packs/prisma-client/fixtures/config/after.ts new file mode 100644 index 0000000..a995465 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/config/after.ts @@ -0,0 +1,10 @@ +import { defineConfig, env } from 'prisma/config'; + +const defaults = { engine: 'classic' }; // @uptide config-removed-options keep + +export default defineConfig({ + schema: 'prisma/schema.prisma', + engine: 'classic', // @uptide config-removed-options + migrations: { path: 'prisma/migrations' }, + datasource: { url: env('DATABASE_URL') }, +}); diff --git a/packages/core/src/packs/prisma-client/fixtures/config/before.ts b/packages/core/src/packs/prisma-client/fixtures/config/before.ts new file mode 100644 index 0000000..a995465 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/config/before.ts @@ -0,0 +1,10 @@ +import { defineConfig, env } from 'prisma/config'; + +const defaults = { engine: 'classic' }; // @uptide config-removed-options keep + +export default defineConfig({ + schema: 'prisma/schema.prisma', + engine: 'classic', // @uptide config-removed-options + migrations: { path: 'prisma/migrations' }, + datasource: { url: env('DATABASE_URL') }, +}); diff --git a/packages/core/src/packs/prisma-client/fixtures/config/filename.txt b/packages/core/src/packs/prisma-client/fixtures/config/filename.txt new file mode 100644 index 0000000..355b786 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/config/filename.txt @@ -0,0 +1 @@ +prisma.config.ts diff --git a/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/after.ts b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/after.ts new file mode 100644 index 0000000..116ff30 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/after.ts @@ -0,0 +1,8 @@ +datasource db { + provider = "mongodb" // @uptide mongodb-unsupported +} + +model Account { + id String @id @map("_id") + provider String // @uptide mongodb-unsupported keep +} diff --git a/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/before.ts b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/before.ts new file mode 100644 index 0000000..116ff30 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/before.ts @@ -0,0 +1,8 @@ +datasource db { + provider = "mongodb" // @uptide mongodb-unsupported +} + +model Account { + id String @id @map("_id") + provider String // @uptide mongodb-unsupported keep +} diff --git a/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/filename.txt b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/filename.txt new file mode 100644 index 0000000..1e52171 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema-mongodb/filename.txt @@ -0,0 +1 @@ +prisma/schema.prisma diff --git a/packages/core/src/packs/prisma-client/fixtures/schema/after.ts b/packages/core/src/packs/prisma-client/fixtures/schema/after.ts new file mode 100644 index 0000000..97b348f --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema/after.ts @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client-js" // @uptide client-js-generator + engineType = "binary" // @uptide engine-type + previewFeatures = ["metrics", "views"] // @uptide metrics-removed +} + +generator docs { + provider = "prisma-docs-generator" // @uptide client-js-generator keep +} + +datasource db { + provider = "postgresql" // @uptide mongodb-unsupported keep + url = env("DATABASE_URL") // @uptide schema-datasource-url + directUrl = env("DIRECT_URL") // @uptide schema-datasource-url + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // @uptide schema-datasource-url + extensions = [uuidOssp(map: "uuid-ossp")] +} + +model Link { + id Int @id @default(autoincrement()) + url String // @uptide schema-datasource-url keep + engineType String // @uptide engine-type keep +} diff --git a/packages/core/src/packs/prisma-client/fixtures/schema/before.ts b/packages/core/src/packs/prisma-client/fixtures/schema/before.ts new file mode 100644 index 0000000..97b348f --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema/before.ts @@ -0,0 +1,23 @@ +generator client { + provider = "prisma-client-js" // @uptide client-js-generator + engineType = "binary" // @uptide engine-type + previewFeatures = ["metrics", "views"] // @uptide metrics-removed +} + +generator docs { + provider = "prisma-docs-generator" // @uptide client-js-generator keep +} + +datasource db { + provider = "postgresql" // @uptide mongodb-unsupported keep + url = env("DATABASE_URL") // @uptide schema-datasource-url + directUrl = env("DIRECT_URL") // @uptide schema-datasource-url + shadowDatabaseUrl = env("SHADOW_DATABASE_URL") // @uptide schema-datasource-url + extensions = [uuidOssp(map: "uuid-ossp")] +} + +model Link { + id Int @id @default(autoincrement()) + url String // @uptide schema-datasource-url keep + engineType String // @uptide engine-type keep +} diff --git a/packages/core/src/packs/prisma-client/fixtures/schema/filename.txt b/packages/core/src/packs/prisma-client/fixtures/schema/filename.txt new file mode 100644 index 0000000..1e52171 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/schema/filename.txt @@ -0,0 +1 @@ +prisma/schema.prisma diff --git a/packages/core/src/packs/prisma-client/fixtures/scripts/after.ts b/packages/core/src/packs/prisma-client/fixtures/scripts/after.ts new file mode 100644 index 0000000..0519231 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/scripts/after.ts @@ -0,0 +1,30 @@ +{ + "name": "app", + "scripts": { + "generate": "prisma generate", // @uptide env-not-loaded keep + "db:push": "dotenv -- prisma db push", // @uptide cli-skip-flags + "db:dev": "dotenv -- prisma migrate dev", // @uptide cli-skip-flags + "test": "vitest run --skip-generate", // @uptide cli-skip-flags keep + "db:deploy": "prisma migrate deploy", // @uptide env-not-loaded + "db:studio": "pnpm exec prisma studio", // @uptide env-not-loaded + "db:deploy:ci": "dotenv -e .env.ci -- prisma migrate deploy", // @uptide env-not-loaded keep + "db:migrate": "dotenv -- prisma migrate dev", // @uptide cli-implicit-steps + "db:sync": "dotenv -- prisma db push && prisma generate", // @uptide cli-implicit-steps keep + "db:diff": "dotenv -- prisma migrate diff --from-url \"$DATABASE_URL\" --to-schema prisma/schema.prisma --script", // @uptide cli-removed-flags + "db:diff:config": "dotenv -- prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma", // @uptide cli-removed-flags keep + "introspect": "npx prisma introspect" // @uptide cli-removed-flags + }, + "prisma": { // @uptide package-json-prisma + "seed": "tsx prisma/seed.ts" + }, + "pnpm": { + "overrides": { + "prisma": { // @uptide package-json-prisma keep + "@prisma/engines": "6.19.3" + } + } + }, + "devDependencies": { + "prisma": "^6.19.3" // @uptide package-json-prisma keep + } +} diff --git a/packages/core/src/packs/prisma-client/fixtures/scripts/before.ts b/packages/core/src/packs/prisma-client/fixtures/scripts/before.ts new file mode 100644 index 0000000..664dda4 --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/scripts/before.ts @@ -0,0 +1,30 @@ +{ + "name": "app", + "scripts": { + "generate": "prisma generate", // @uptide env-not-loaded keep + "db:push": "dotenv -- prisma db push --skip-generate", // @uptide cli-skip-flags + "db:dev": "dotenv -- prisma migrate dev --skip-seed --skip-generate", // @uptide cli-skip-flags + "test": "vitest run --skip-generate", // @uptide cli-skip-flags keep + "db:deploy": "prisma migrate deploy", // @uptide env-not-loaded + "db:studio": "pnpm exec prisma studio", // @uptide env-not-loaded + "db:deploy:ci": "dotenv -e .env.ci -- prisma migrate deploy", // @uptide env-not-loaded keep + "db:migrate": "dotenv -- prisma migrate dev", // @uptide cli-implicit-steps + "db:sync": "dotenv -- prisma db push && prisma generate", // @uptide cli-implicit-steps keep + "db:diff": "dotenv -- prisma migrate diff --from-url \"$DATABASE_URL\" --to-schema prisma/schema.prisma --script", // @uptide cli-removed-flags + "db:diff:config": "dotenv -- prisma migrate diff --from-config-datasource --to-schema prisma/schema.prisma", // @uptide cli-removed-flags keep + "introspect": "npx prisma introspect" // @uptide cli-removed-flags + }, + "prisma": { // @uptide package-json-prisma + "seed": "tsx prisma/seed.ts" + }, + "pnpm": { + "overrides": { + "prisma": { // @uptide package-json-prisma keep + "@prisma/engines": "6.19.3" + } + } + }, + "devDependencies": { + "prisma": "^6.19.3" // @uptide package-json-prisma keep + } +} diff --git a/packages/core/src/packs/prisma-client/fixtures/scripts/filename.txt b/packages/core/src/packs/prisma-client/fixtures/scripts/filename.txt new file mode 100644 index 0000000..ec6d3cd --- /dev/null +++ b/packages/core/src/packs/prisma-client/fixtures/scripts/filename.txt @@ -0,0 +1 @@ +package.json diff --git a/packages/core/src/packs/prisma-client/ground-truth.json b/packages/core/src/packs/prisma-client/ground-truth.json new file mode 100644 index 0000000..dc0267b --- /dev/null +++ b/packages/core/src/packs/prisma-client/ground-truth.json @@ -0,0 +1,5 @@ +{ + "$comment": "Public repositories at the commit before they upgraded @prisma/client from >=6 <7 to >=7 <8, and every finding the pack must report there. See docs/packs.md, \"Ground truth\".", + "package": "@prisma/client", + "repos": [] +} diff --git a/packages/core/src/packs/prisma-client/index.ts b/packages/core/src/packs/prisma-client/index.ts new file mode 100644 index 0000000..27cf8cd --- /dev/null +++ b/packages/core/src/packs/prisma-client/index.ts @@ -0,0 +1,384 @@ +import { join, resolve } from 'node:path'; +import { projectFiles } from '../../check/importers.js'; +import { minVersion, satisfies, validRange } from '../../fetch/range.js'; +import { type BehaviorNote, definePack, detected, type Pack, type PackRule } from '../contract.js'; +import type { PackContext } from '../types.js'; +import { + ADAPTER_RENAMES, + adapterRenameSites, + CONFIG, + clientAdapterSites, + clientJsSites, + configOptionSites, + datasourceUrlSites, + engineTypeSites, + envNotLoadedSites, + implicitStepSites, + mappedEnumMembers, + metricsSites, + middlewareSites, + mongodbSites, + packageJsonPrismaSites, + REMOVED_ENTRYPOINT, + removedEntrypointSites, + removedFlagSites, + rewriteSkipFlags, + schemaEnvReads, + schemaProperties, + skipFlagSites, + urlParamSites, +} from './detect.js'; + +const read = 'read 2026-10-20'; +const web = + 'https://github.com/prisma/web/blob/42f314651cb659e885fdf59554196b2ea8efa75d/apps/docs/content/docs'; +const GUIDE = `${web}/guides/upgrade-prisma-orm/v7.mdx`; +const release = (tag: string) => `https://github.com/prisma/orm/releases/tag/${tag}`; + +/** + * Every rule finds its sites with `detect`: the compiler-visible changes live in the generated + * client, which `check` does not regenerate at 7 (RESEARCH.md, "How the compiler sees Prisma"). + */ +const rules: PackRule[] = [ + { + id: 'client-adapter', + summary: + '`new PrismaClient()` needs a driver adapter (or `accelerateUrl`) in 7, and `datasources`/`datasourceUrl` are removed: without one the constructor throws', + severity: 'breaking', + kinds: ['type', 'signature'], + symbols: /^TS(?:2353|2554|2345)$/, + message: /'adapter'|datasources|datasourceUrl|Expected 1 arguments/, + detect: clientAdapterSites, + guide: + "Pass the driver adapter for the schema's database provider: `new PrismaClient({ adapter: new PrismaPg({ connectionString: process.env.DATABASE_URL }) })` (`@prisma/adapter-pg`; SQLite `PrismaBetterSqlite3({ url })` from `@prisma/adapter-better-sqlite3`, MySQL/MariaDB `PrismaMariaDb`, SQL Server `PrismaMssql`, Neon `PrismaNeon`). The connection string is the value `datasources.db.url`/`datasourceUrl` passed, else the variable the schema's `url = env(…)` named, read where the code runs (7 loads no .env file). Keep every other option (`log`, `errorFormat`, `omit`, `transactionOptions`). With an Accelerate URL (`prisma://`, `prisma+postgres://`) pass `accelerateUrl` instead and keep `.$extends(withAccelerate())`; never give that URL to an adapter. Pool size and timeouts now come from the driver (connection-url-params). The adapter package is a new dependency: if it is not installed, say so in the decision for the owner instead of editing around it. Never cast the options to `any`.", + }, + { + id: 'schema-datasource-url', + summary: + '`url`, `directUrl` and `shadowDatabaseUrl` are no longer allowed in a schema datasource (P1012): the CLI reads them from `prisma.config.ts`', + severity: 'breaking', + kinds: [], + symbols: /$^/, + detect: datasourceUrlSites, + guide: + 'Remove the property from the datasource block and put the URL in `prisma.config.ts` next to package.json: `import { defineConfig, env } from "prisma/config"; export default defineConfig({ schema: "prisma/schema.prisma", datasource: { url: env("DATABASE_URL") } })`, keeping a `migrations` block if the project has one. Where `directUrl` was set, the CLI uses `url` for migrations: give `url` the direct (non-pooled) URL and keep the pooled one for the runtime adapter. `shadowDatabaseUrl` moves to `datasource.shadowDatabaseUrl`. `env()` throws when the variable is unset; `process.env.X ?? ""` lets `prisma generate` run without a database. 7 loads no .env file: add `import "dotenv/config"` only when the project already has dotenv.', + }, + { + id: 'package-json-prisma', + summary: + 'the `"prisma"` key of package.json (`schema`, `seed`) is ignored in 7: the schema path and the seed command move to `prisma.config.ts`', + severity: 'breaking', + kinds: [], + symbols: /$^/, + detect: packageJsonPrismaSites, + guide: + 'Move `prisma.schema` to `schema` and `prisma.seed` to `migrations.seed` in `prisma.config.ts`, then remove the `"prisma"` key. The command stays as it is.', + }, + { + id: 'cli-skip-flags', + summary: + '`--skip-generate` (`migrate dev`, `db push`) and `--skip-seed` (`migrate dev`) are removed in 7, which never generates or seeds there', + severity: 'breaking', + kinds: [], + symbols: /$^/, + detect: skipFlagSites, + rewrite: (text, finding) => rewriteSkipFlags(text, finding), + guide: 'Drop the flag: 7 does what it asked for by default.', + }, + { + id: 'cli-removed-flags', + summary: + 'CLI flags and commands removed in 7: `db execute --schema/--url`, `migrate diff --from-url/--to-url/--*-schema-datasource/--*-schema-datamodel/--shadow-database-url`, `generate --data-proxy/--accelerate/--no-engine/--allow-no-models`, `db pull --local-d1`, `prisma introspect`, and the removed `PRISMA_*` variables', + severity: 'breaking', + kinds: [], + symbols: /$^/, + detect: removedFlagSites, + guide: + '`db execute` and `migrate diff` read the database from `prisma.config.ts`: `--from-url`/`--from-schema-datasource` become `--from-config-datasource` (`--to-…` the same), `--*-schema-datamodel` becomes `--*-schema`, `--shadow-database-url` moves to `datasource.shadowDatabaseUrl`. `prisma introspect` is `prisma db pull`. Drop `generate --data-proxy/--accelerate/--no-engine` and the removed `PRISMA_*` variables.', + }, + { + id: 'config-removed-options', + summary: + '`engine` and `adapter` are removed from `prisma.config.ts` in 7: the client takes the adapter', + severity: 'breaking', + kinds: ['type'], + symbols: /^TS2353$/, + message: /'(?:engine|adapter)' does not exist in type 'PrismaConfig'/, + detect: configOptionSites, + guide: + 'Remove `engine` (7 has one engine). An `adapter` in the config becomes `datasource.url` for the CLI, and the adapter is passed to `new PrismaClient({ adapter })` instead.', + }, + { + id: 'mongodb-unsupported', + summary: 'Prisma 7 does not support MongoDB yet: stay on Prisma 6', + severity: 'breaking', + kinds: [], + symbols: /$^/, + detect: mongodbSites, + guide: + 'Do not upgrade this project: the upgrade guide says to stay on Prisma 6 for MongoDB. Record it as a decision for the owner.', + }, + { + id: 'client-middleware', + summary: + '`$use` middleware and `Prisma.Middleware` are removed in 7: query extensions (`$extends`) replace them', + severity: 'breaking', + kinds: ['type', 'removed'], + symbols: /(?:\$use|Middleware|^TS2339$|^TS2694$)/, + message: /(?:^$|\$use|Middleware)/, + detect: middlewareSites, + guide: + '`prisma.$use(async (params, next) => …)` becomes a query extension: `const prisma = base.$extends({ query: { $allModels: { async $allOperations({ model, operation, args, query }) { … return query(args) } } } })`, where `params.model`/`params.action`/`params.args` are `model`/`operation`/`args` and `next(params)` is `query(args)`. `$extends` returns a new client: export and use that one; the base client no longer runs the middleware. Keep the order of several middlewares (the first `$use` is the outermost extension).', + }, + { + id: 'metrics-removed', + summary: + 'the metrics preview feature is removed in 7: `$metrics` and `previewFeatures = ["metrics"]` (P1012) are gone', + severity: 'breaking', + kinds: ['type'], + symbols: /(?:\$metrics|^TS2339$)/, + message: /(?:^$|\$metrics)/, + detect: metricsSites, + guide: + 'Remove "metrics" from `previewFeatures`. There is no Prisma replacement for `$metrics`: read pool metrics from the driver (`pg`\'s `Pool` counts) or count queries in a `$extends` query extension, and say which in the decision for the owner.', + }, + { + id: 'adapter-renames', + summary: `driver adapter classes renamed in 7: ${Object.entries(ADAPTER_RENAMES) + .map(([from, to]) => `${from} → ${to}`) + .join(', ')}`, + severity: 'breaking', + kinds: ['removed', 'renamed'], + symbols: new RegExp(`(?:^|[:#.])(?:${Object.keys(ADAPTER_RENAMES).join('|')})$`), + detect: adapterRenameSites, + guide: `Import the new name: ${Object.entries(ADAPTER_RENAMES) + .map(([from, to]) => `\`${from}\` is \`${to}\``) + .join(', ')}; the constructor arguments are the same.`, + }, + { + id: 'removed-entrypoints', + summary: + 'entry points removed in 7: `@prisma/client/runtime/library`, `runtime/binary`, `runtime/edge`, `runtime/react-native`, `/wasm`, `/react-native`', + severity: 'breaking', + kinds: ['removed', 'type'], + symbols: new RegExp(`${REMOVED_ENTRYPOINT.source.slice(1, -1)}|^TS2307$`), + message: /(?:^$|@prisma\/client\/(?:runtime|wasm|react-native))/, + detect: removedEntrypointSites, + guide: + '`@prisma/client/runtime/library` is `@prisma/client/runtime/client` (already exported by 6.19): `Decimal`, `PrismaClientKnownRequestError`, `JsonValue`, `Sql` and the other public names are there; the Decimal.js internals, `Metric*` and `objectEnumValues` are not. `@prisma/client/wasm` is `@prisma/client/edge` in 7 (`/edge` now means what `/wasm` did). React Native and the binary engine are gone.', + }, + { + id: 'client-js-generator', + summary: + 'the `prisma-client-js` generator is deprecated in 7: `prisma-client` generates the client into the project, with an `output` path', + severity: 'deprecated', + kinds: [], + symbols: /$^/, + detect: clientJsSites, + guide: + '`provider = "prisma-client"` with `output = "../src/generated/prisma"` (any path in the project), then import `PrismaClient` and `Prisma` from `/client` instead of `@prisma/client`. Its output is ESM unless `moduleFormat = "cjs"`. It does not generate `Prisma.dmmf` or `Prisma.validator`. 7 still supports `prisma-client-js`: switch only when the owner wants to.', + }, + { + id: 'engine-type', + summary: + '`engineType` in the generator is unused in 7 (the library and binary engines are removed)', + severity: 'deprecated', + kinds: [], + symbols: /$^/, + detect: engineTypeSites, + guide: 'Remove the `engineType` line: 7 ignores it.', + }, +]; + +const files = (context: PackContext) => context.inventory ?? []; +const schemas = (context: PackContext) => files(context).filter((f) => f.file.endsWith('.prisma')); + +/** Where the repository allows Node below 20.19 or TypeScript below 5.4. */ +function belowMinimums(context: PackContext): string[] { + const hits: string[] = []; + for (const { file, text } of files(context)) { + const ranges: [string, string | undefined, string][] = []; + if (/(?:^|\/)package\.json$/.test(file)) { + try { + const manifest = JSON.parse(text) as { + engines?: { node?: string }; + dependencies?: Record; + devDependencies?: Record; + }; + ranges.push(['Node', manifest.engines?.node, '20.19.0']); + ranges.push([ + 'TypeScript', + manifest.devDependencies?.typescript ?? manifest.dependencies?.typescript, + '5.4.0', + ]); + } catch {} + } else if (/(?:^|\/)\.(?:nvmrc|node-version)$/.test(file)) + ranges.push(['Node', text.trim().replace(/^v/, ''), '20.19.0']); + for (const [what, range, min] of ranges) { + const lowest = range && validRange(range) ? minVersion(range) : undefined; + if (lowest && !satisfies(lowest, `>=${min}`)) + hits.push(`\`${file}\` allows ${what} ${JSON.stringify(range)}`); + } + } + return hits; +} + +const behavior: BehaviorNote[] = [ + { + id: 'env-not-loaded', + summary: + 'Prisma 7 loads no .env file: neither the CLI (`migrate`, `db`, `studio`) nor the client at runtime, which on 6 read the URL its schema named. A script that relied on it needs `dotenv` (or `--env-file`), and the app must have the variable set where the client is created', + reported: ['finding', 'decision'], + severity: 'deprecated', + detect: envNotLoadedSites, + applies: (context) => { + const reads = schemas(context).flatMap(({ file, text }) => + schemaEnvReads(text).map((r) => `\`${file}:${r.line}\` reads ${r.name} with env()`), + ); + return reads.length + ? `Prisma 7 loads no .env file, for the CLI or for the client at runtime. On 6 the client loaded it for the URL its schema read: ${reads.join('; ')}. Make sure that variable is set where the app runs (or load the file with dotenv), and run CLI scripts with it loaded` + : false; + }, + }, + { + id: 'cli-implicit-steps', + summary: + '`prisma migrate dev` and `migrate reset` no longer run the seed script, and `migrate dev` and `db push` no longer run `prisma generate`: run `prisma db seed` and `prisma generate` explicitly where the workflow relied on them', + reported: ['finding', 'decision'], + severity: 'deprecated', + detect: implicitStepSites, + }, + { + id: 'connection-url-params', + summary: + 'the v6 connection URL parameters `connection_limit`, `pool_timeout`, `connect_timeout`, `max_idle_connection_lifetime` and `max_connection_lifetime` are not read by the 7 driver adapters: set the pool on the adapter (`max`, `connectionTimeoutMillis`, `idleTimeoutMillis` for pg), whose defaults differ (pg: no connection timeout, where 6 had 5 s)', + reported: ['finding', 'decision'], + severity: 'deprecated', + detect: urlParamSites, + }, + { + id: 'ssl-validation', + summary: + 'with the pg driver, Prisma 7 validates the database server certificate, where 6 ignored an invalid one: a self-signed certificate now fails with P1010 unless the CA is trusted (`NODE_EXTRA_CA_CERTS`) or the adapter gets `ssl: { rejectUnauthorized: false }`', + reported: ['decision'], + applies: (context) => + schemas(context).some(({ text }) => + schemaProperties(text).some( + (p) => + p.block === 'datasource' && + p.key === 'provider' && + /"(?:postgresql|postgres|cockroachdb)"/.test(p.value), + ), + ), + }, + { + id: 'mapped-enum-values', + summary: + 'Prisma 7.0.0 to 7.2.x generate an enum member with `@map` as its mapped value ("pending"), where 6 and 7.3.0+ use its name ("PENDING"): target 7.3.0 or later to keep the values', + reported: ['decision'], + applies: (context) => { + if (satisfies(context.to, '>=7.3.0')) return false; + const sites = schemas(context).flatMap(({ file, text }) => + mappedEnumMembers(text).map((line) => `\`${file}:${line}\``), + ); + return sites.length + ? `Prisma ${context.to} generates mapped enum members as their @map value, unlike 6 and 7.3.0+: ${sites.join(', ')}. Target 7.3.0 or later` + : false; + }, + }, + { + id: 'runtime-minimums', + summary: 'Prisma 7 requires Node.js ^20.19, ^22.12 or >=24, and TypeScript 5.4 or later', + reported: ['decision'], + applies: (context) => { + const hits = belowMinimums(context); + return hits.length + ? `Prisma 7 requires Node.js ^20.19 || ^22.12 || >=24 and TypeScript >=5.4: ${hits.join('; ')}` + : false; + }, + }, +]; + +const base = definePack({ + meta: { + package: '@prisma/client', + from: '>=6 <7', + to: '>=7 <8', + sources: [ + { title: `Prisma ORM upgrade guide to v7, prisma/web 42f3146 (${read})`, url: GUIDE }, + { title: `Prisma ORM 7.0.0 release notes (${read})`, url: release('7.0.0') }, + { + title: `Prisma ORM 7.3.0 release notes, mapped enums reverted (${read})`, + url: release('7.3.0'), + }, + { + title: `Connection pool defaults for v7 driver adapters, prisma/web 42f3146 (${read})`, + url: `${web}/orm/v7/prisma-client/setup-and-configuration/databases-connections/connection-pool.mdx`, + }, + { + title: `Environment variables in Prisma ORM 6, prisma/web 42f3146 (${read})`, + url: `${web}/orm/v6/more/dev-environment/environment-variables.mdx`, + }, + { + title: `Published declarations and builds: @prisma/client 6.19.3, 7.10.1; prisma 6.19.3, 7.10.1 (${read})`, + url: 'https://registry.npmjs.org/@prisma/client', + }, + ], + maintainer: 'uptide-dev', + }, + defaultTarget: '7.10.1', + inventory: [ + 'package.json', + '**/*.prisma', + 'prisma.config.{ts,mts,cts,js,mjs,cjs}', + '{.nvmrc,.node-version}', + ], + // `prisma` and `@prisma/client` move together (the guide's "Update packages"); every adapter is + // published at each client version and renamed its classes at 7.0.0 (RESEARCH.md). + releaseGroup: [ + { name: 'prisma', source: `${GUIDE}#L36-L44` }, + ...[ + 'pg', + 'neon', + 'planetscale', + 'd1', + 'libsql', + 'better-sqlite3', + 'mariadb', + 'mssql', + 'ppg', + ].map((name) => ({ name: `@prisma/adapter-${name}`, source: release('7.0.0') })), + ], + rules, + behavior, + instructions: + 'Sources read 2026-10-20 (prisma/web 42f3146, release notes 7.0.0 to 7.10.1); see RESEARCH.md. Migrate only the reported site, and keep what the code does: the same database, the same connection string, the same client options. Add no dependency: when the site needs a driver adapter or dotenv the project does not have, write that into the decision for the owner. Run `prisma generate` after a schema or config change. Never cast PrismaClient options or the client to any, and never suppress a diagnostic to get a clean compile.', +}); + +/** A source file worth parsing: it names the client, an adapter, an entry point or a URL parameter. */ +const MENTIONS = + /PrismaClient|\$use|\$metrics|Prisma\.Middleware|@prisma\/|connection_limit|pool_timeout|connect_timeout|_connection_lifetime/; + +/** + * @prisma/client >=6 <7 → >=7 <8. `check` finds no usage of the package (its types are the + * generated client), so the pack reads the workspace's TypeScript project itself, as well as + * its inventory. Where a Prisma config already existed on 6, the CLI loaded no .env file then + * either: `env-not-loaded` has nothing to report for its scripts. + */ +export const prismaClientPack: Pack = { + ...base, + runtimeFindings(input) { + const inventory = input.files ?? []; + const configured = inventory.some((f) => CONFIG.test(f)); + const sources = projectFiles(resolve(input.root, input.workspace)).files; + const all = [...new Set([...input.usages.map((u) => u.file), ...inventory, ...sources])].sort(); + return all.flatMap((file) => { + const text = input.read(join(input.workspace, file)); + if (text === undefined) return []; + if (/\.[cm]?[jt]sx?$/.test(file) && !CONFIG.test(file) && !MENTIONS.test(text)) return []; + return detected(base, { file, text, from: input.from, to: input.to }).filter( + (f) => !(configured && f.rule === 'env-not-loaded'), + ); + }); + }, +}; diff --git a/packages/core/src/packs/prisma-client/prisma-client.test.ts b/packages/core/src/packs/prisma-client/prisma-client.test.ts new file mode 100644 index 0000000..47f8582 --- /dev/null +++ b/packages/core/src/packs/prisma-client/prisma-client.test.ts @@ -0,0 +1,106 @@ +import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { afterAll, describe, expect, it } from 'vitest'; +import { runFixtures } from '../tooling/fixtures.js'; +import { prismaClientPack } from './index.js'; + +it('@prisma/client: every fixture rewrites and detects exactly its marked sites', () => { + const result = runFixtures(prismaClientPack, fileURLToPath(new URL('.', import.meta.url))); + expect(result.cases.length).toBeGreaterThan(0); + expect(result.unknown).toEqual([]); + expect(result.rewriteFailures).toEqual([]); + expect(result.falsePositives).toEqual([]); + expect(result.falseNegatives).toEqual([]); +}); + +describe('runtime findings', () => { + const root = mkdtempSync(join(tmpdir(), 'uptide-prisma-')); + afterAll(() => rmSync(root, { recursive: true, force: true })); + const write = (files: Record) => { + for (const [file, text] of Object.entries(files)) { + mkdirSync(dirname(join(root, file)), { recursive: true }); + writeFileSync(join(root, file), text); + } + }; + const run = (inventory: string[]) => + ( + prismaClientPack.runtimeFindings?.({ + root, + workspace: '.', + from: '6.19.3', + to: '7.10.1', + installedDir: '', + targetDir: '', + // `check` finds no usage of @prisma/client: its types are the generated client. + usages: [], + files: inventory, + read: (file) => { + try { + return readFileSync(join(root, file), 'utf8'); + } catch { + return undefined; + } + }, + }) ?? [] + ).map((f) => `${f.usage.file}:${f.usage.line} ${f.rule}`); + + write({ + 'tsconfig.json': '{ "include": ["src"] }', + 'src/db.ts': + "import { PrismaClient } from '@prisma/client';\nexport const db = new PrismaClient();\n", + 'src/other.ts': 'export const PrismaClient = 1;\n', + 'package.json': '{\n "scripts": {\n "db:deploy": "prisma migrate deploy"\n }\n}\n', + }); + + it('reads the TypeScript project, not only importing files', () => { + expect(run(['package.json'])).toEqual([ + 'package.json:3 env-not-loaded', + 'src/db.ts:2 client-adapter', + ]); + }); + + it('reports no .env script where a Prisma config already existed on 6', () => { + write({ 'prisma.config.ts': "export default { schema: 'prisma/schema.prisma' };\n" }); + expect(run(['package.json', 'prisma.config.ts'])).toEqual(['src/db.ts:2 client-adapter']); + }); +}); + +describe('decisions', () => { + const decide = (id: string, to: string, inventory: Record) => + prismaClientPack.behavior + .find((b) => b.id === id) + ?.applies?.({ + from: '6.19.3', + to, + includeDeprecated: false, + inventory: Object.entries(inventory).map(([file, text]) => ({ file, text })), + }); + const schema = + 'datasource db {\n provider = "postgresql"\n url = env("DATABASE_URL")\n}\n\nenum Status {\n PENDING @map("pending")\n}\n'; + + it('names where the app read its URL on 6', () => { + expect(decide('env-not-loaded', '7.10.1', { 'prisma/schema.prisma': schema })).toMatch( + /`prisma\/schema.prisma:3` reads DATABASE_URL with env\(\)/, + ); + }); + + it('warns about mapped enums only for a target that changes them', () => { + expect(decide('mapped-enum-values', '7.2.0', { 'prisma/schema.prisma': schema })).toMatch( + /prisma\/schema.prisma:7/, + ); + expect(decide('mapped-enum-values', '7.10.1', { 'prisma/schema.prisma': schema })).toBe(false); + }); + + it('names a Node or TypeScript the repository allows below the minimums', () => { + const manifest = JSON.stringify({ + engines: { node: '>=18' }, + devDependencies: { typescript: '^5.9.3' }, + }); + expect(decide('runtime-minimums', '7.10.1', { 'package.json': manifest })).toMatch( + /`package.json` allows Node ">=18"$/, + ); + expect(decide('runtime-minimums', '7.10.1', { '.nvmrc': '22\n' })).toBe(false); + }); +}); diff --git a/packages/core/src/packs/prisma-client/verification.json b/packages/core/src/packs/prisma-client/verification.json new file mode 100644 index 0000000..bfe31eb --- /dev/null +++ b/packages/core/src/packs/prisma-client/verification.json @@ -0,0 +1,32 @@ +{ + "status": "candidate", + "truth": "sha256-b16bb874a2ced0eaa8a7a537655bcfb5f8584e406087a4234401ab5df9c1b43f", + "repos": 0, + "breaking": { + "precision": 1, + "recall": 1, + "falsePositives": 0, + "truePositives": 0, + "falseNegatives": 0 + }, + "behavior": { + "precision": 1, + "recall": 1, + "falsePositives": 0, + "truePositives": 0, + "falseNegatives": 0 + }, + "class": "breaking", + "breakingSites": 0, + "breakingFoundAny": 0, + "breakingFound": 0, + "breakingScored": 0, + "behaviorSites": 0, + "unexercised": ["example-rename"], + "mechanical": { + "example-rename": { + "truePositives": 0, + "falsePositives": 0 + } + } +} diff --git a/packages/core/src/packs/registry.ts b/packages/core/src/packs/registry.ts index bba6c8c..ed91e24 100644 --- a/packages/core/src/packs/registry.ts +++ b/packages/core/src/packs/registry.ts @@ -16,6 +16,8 @@ import { nextPack } from './next/index.js'; import nextVerification from './next/verification.json' with { type: 'json' }; import { openaiPack } from './openai/index.js'; import openaiVerification from './openai/verification.json' with { type: 'json' }; +import { prismaClientPack } from './prisma-client/index.js'; +import prismaClientVerification from './prisma-client/verification.json' with { type: 'json' }; import { reactPack } from './react/index.js'; import reactVerification from './react/verification.json' with { type: 'json' }; import { reactRouterPack } from './react-router/index.js'; @@ -49,6 +51,11 @@ export const REGISTRY: readonly RegisteredPack[] = [ }, { dir: 'next', pack: nextPack, verification: nextVerification as PackVerification }, { dir: 'openai', pack: openaiPack, verification: openaiVerification as PackVerification }, + { + dir: 'prisma-client', + pack: prismaClientPack, + verification: prismaClientVerification as PackVerification, + }, { dir: 'react', pack: reactPack, verification: reactVerification as PackVerification }, { dir: 'react-router', From cac9bca68a75ccb24c7f0b1916e0d665c4b7abb8 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:20:54 -0700 Subject: [PATCH 3/9] test(prisma): ground truth from seven public Prisma 6 to 7 upgrades Committed before the first score. BearStudio/start-ui-web #781, jqlang/playground #344, openwallet-foundation-labs/ehr-wallet #114, bcgov/nr-permitconnect-navigator-service #574, basaltkit/basalt #188, AlexAgo83/cr-league #30, MarvinLeRouge/HexaRot #160: every site read by hand from each upgrade commit; the compiler draft against a client generated at 6 is never a site. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../src/packs/prisma-client/ground-truth.json | 176 +++++++++++++++++- 1 file changed, 174 insertions(+), 2 deletions(-) diff --git a/packages/core/src/packs/prisma-client/ground-truth.json b/packages/core/src/packs/prisma-client/ground-truth.json index dc0267b..4c44045 100644 --- a/packages/core/src/packs/prisma-client/ground-truth.json +++ b/packages/core/src/packs/prisma-client/ground-truth.json @@ -1,5 +1,177 @@ { - "$comment": "Public repositories at the commit before they upgraded @prisma/client from >=6 <7 to >=7 <8, and every finding the pack must report there. See docs/packs.md, \"Ground truth\".", + "$comment": "Public repositories at the commit before they upgraded @prisma/client from >=6 <7 to >=7 <8, and every finding the pack must report there. See docs/packs.md, \"Ground truth\". Drafted with packs:truth; the lines the upgrade changed in schema.prisma, package.json and prisma.config.* were read from the same `git diff -U0` by hand (packs:truth lists source files and tsconfigs only), with the same rules. The compiler draft is never a site here: with @prisma/client 7 linked next to a client generated at 6 it reports TS2305/TS7006/TS7031/TS2339 that a real upgrade, which regenerates the client, never has (RESEARCH.md).", "package": "@prisma/client", - "repos": [] + "repos": [ + { + "repo": "BearStudio/start-ui-web", + "commit": "7a4e236f6ac3ac341c6ef1802ea6c8c7bbc585d9", + "migration": "https://github.com/BearStudio/start-ui-web/pull/781", + "from": "6.19.2", + "to": "7.10.0", + "with": { "prisma": "7.10.0" }, + "why": "A TanStack Start app already on the prisma-client generator: the datasource URL moves to prisma.config.ts, the client gets the pg adapter, and the CLI scripts load .env with dotenv-cli and run generate themselves. The vite plugin that copied the engine binaries (removed in 7) and the @@map reordering are cleanup, not sites.", + "findings": [ + { "file": "prisma/schema.prisma", "line": 12, "rule": "schema-datasource-url" }, + { + "file": "src/server/db/index.ts", + "line": 16, + "rule": "client-adapter", + "note": "anchor: adapter added to the constructor options" + }, + { + "file": "package.json", + "line": 58, + "rule": "env-not-loaded", + "note": "db:push now runs under dotenv" + }, + { + "file": "package.json", + "line": 58, + "rule": "cli-implicit-steps", + "note": "db:push now runs gen:prisma after the push" + }, + { + "file": "package.json", + "line": 59, + "rule": "env-not-loaded", + "note": "db:ui (studio) now runs under dotenv" + } + ] + }, + { + "repo": "jqlang/playground", + "commit": "48a8e191766c62477a54582820745983d876e6ed", + "migration": "https://github.com/jqlang/playground/pull/344", + "from": "6.19.3", + "to": "7.8.0", + "with": { "prisma": "7.8.0" }, + "why": "A Next.js app on prisma-client-js: the smallest real upgrade, the schema URL and one client with the pg adapter.", + "findings": [ + { "file": "prisma/schema.prisma", "line": 14, "rule": "schema-datasource-url" }, + { + "file": "src/lib/prisma.ts", + "line": 11, + "rule": "client-adapter", + "note": "anchor: adapter added to the constructor options" + } + ] + }, + { + "repo": "openwallet-foundation-labs/ehr-wallet", + "commit": "a574d2bae9aff47a146886d3728ab57c120196cf", + "migration": "https://github.com/openwallet-foundation-labs/ehr-wallet/pull/114", + "from": "6.19.3", + "to": "7.10.0", + "with": { "prisma": "7.10.0" }, + "why": "A Next.js pages app with a client per API route, all without options: each throws at construction on 7. It also moved the seed out of package.json and switched to the prisma-client generator (better-sqlite3 adapter). The import and jest mock changes follow the generator switch and are not sites.", + "findings": [ + { "file": "prisma/schema.prisma", "line": 5, "rule": "client-js-generator" }, + { "file": "prisma/schema.prisma", "line": 11, "rule": "schema-datasource-url" }, + { "file": "package.json", "line": 25, "rule": "package-json-prisma" }, + { + "file": "lib/prisma.ts", + "line": 9, + "rule": "client-adapter", + "note": "lines 7-8 were only rewrapped into createPrismaClient(), the repository's own refactor" + }, + { "file": "pages/api/fhir/Encounter/index.ts", "line": 6, "rule": "client-adapter" }, + { "file": "pages/api/fhir/Patient/[id].ts", "line": 6, "rule": "client-adapter" }, + { + "file": "pages/api/fhir/Patient/[id]/[operation].ts", + "line": 6, + "rule": "client-adapter" + }, + { "file": "pages/api/fhir/Patient/index.ts", "line": 6, "rule": "client-adapter" }, + { "file": "pages/api/shared-data/index.ts", "line": 8, "rule": "client-adapter" }, + { "file": "scripts/seed-shared-data.ts", "line": 5, "rule": "client-adapter" } + ] + }, + { + "repo": "bcgov/nr-permitconnect-navigator-service", + "commit": "6d63485083292eb5771b3bede1443a2b36c3e558", + "migration": "https://github.com/bcgov/nr-permitconnect-navigator-service/pull/574", + "directory": "app", + "from": "6.19.3", + "to": "7.10.0", + "with": { "prisma": "7.10.0" }, + "why": "An Express API: datasourceUrl becomes the pg adapter, connection_limit moves from the URL to the pool's max, the schema path leaves package.json. It also switched to the prisma-client generator; the Prisma.dmmf, Prisma.validator and import rewrites follow that switch (prisma-client-js 7 still has them), and the reporting SQL changes are unrelated.", + "findings": [ + { "file": "app/src/db/prisma/schema.prisma", "line": 2, "rule": "client-js-generator" }, + { "file": "app/src/db/prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, + { "file": "app/package.json", "line": 132, "rule": "package-json-prisma" }, + { + "file": "app/src/db/database.ts", + "line": 21, + "rule": "connection-url-params", + "note": "&connection_limit= dropped from the URL, max set on the adapter" + }, + { "file": "app/src/db/database.ts", "line": 35, "rule": "client-adapter" } + ] + }, + { + "repo": "basaltkit/basalt", + "commit": "2c255d3fd97bf2e230fb8e5685b9ded799f2a3c4", + "migration": "https://github.com/basaltkit/basalt/pull/188", + "from": "6.19.3", + "to": "7.9.1", + "with": { "prisma": "7.9.1" }, + "why": "A framework monorepo whose integration harness (apps/pg-integration) loads the generated client with a dynamic import: the clients get the pg adapter, and db:push loses --skip-generate. Scored from the root: its lockfile is there. The hand-typed constructor signatures it widened (new () => to new (opts?) =>) follow its own fix and are not sites. The reference schemas under packages/*-prisma, which no workspace with @prisma/client loads, did not change.", + "findings": [ + { + "file": "apps/pg-integration/prisma/schema.prisma", + "line": 9, + "rule": "schema-datasource-url" + }, + { "file": "apps/pg-integration/package.json", "line": 8, "rule": "cli-skip-flags" }, + { + "file": "apps/pg-integration/tests/stores.integration.test.ts", + "line": 30, + "rule": "client-adapter" + }, + { + "file": "apps/pg-integration/tests/tenancy.integration.test.ts", + "line": 35, + "rule": "client-adapter" + } + ] + }, + { + "repo": "AlexAgo83/cr-league", + "commit": "67861c3c23d75e26435ae66ff21334b7e97011ef", + "migration": "https://github.com/AlexAgo83/cr-league/pull/30", + "from": "6.19.3", + "to": "7.9.1", + "with": { "prisma": "7.9.1" }, + "why": "An npm-workspaces monorepo whose API tests point clients at other databases with datasources: each becomes the pg adapter. prisma.config.ts existed on 6 (the datasource it gained is the schema URL moving, not a site of its own). The Node engines bump and the ?schema= handling are the repository's choice; no source covers the latter.", + "findings": [ + { "file": "prisma/schema.prisma", "line": 7, "rule": "schema-datasource-url" }, + { "file": "apps/api/src/app.postgres.test.ts", "line": 18, "rule": "client-adapter" }, + { "file": "apps/api/src/app.postgres.test.ts", "line": 28, "rule": "client-adapter" }, + { "file": "apps/api/src/app.postgres.test.ts", "line": 39, "rule": "client-adapter" }, + { "file": "apps/api/src/db/client.ts", "line": 3, "rule": "client-adapter" } + ] + }, + { + "repo": "MarvinLeRouge/HexaRot", + "commit": "117ef409197e5cb3c8e308d8bc9cd09015c360a0", + "migration": "https://github.com/MarvinLeRouge/HexaRot/pull/160", + "directory": "backend", + "from": "6.19.2", + "to": "7.10.0", + "with": { "prisma": "7.10.0" }, + "withoutGenerated": "backend/prisma.config.ts reads env(\"DATABASE_URL\"), which throws when the variable is unset: prisma generate needs the project's database URL at this commit.", + "why": "A NestJS backend already on prisma-client and prisma.config.ts: engine \"classic\" leaves the config (TS2353 at 7), the seed moves out of package.json, the clients get the pg adapter. PrismaService (extends PrismaClient) gained a constructor: an insertion into a class body, which the ground-truth rules do not count as a site.", + "findings": [ + { "file": "backend/prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, + { + "file": "backend/package.json", + "line": 108, + "rule": "package-json-prisma", + "note": "line 107 only loses the comma before the removed key" + }, + { "file": "backend/prisma.config.ts", "line": 12, "rule": "config-removed-options" }, + { "file": "backend/prisma/seed.ts", "line": 3, "rule": "client-adapter" } + ] + } + ] } From 04ac449d3b851e818518f0e4a836fe4ab42dd362 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:25:24 -0700 Subject: [PATCH 4/9] test(prisma): name directory entries' files from the directory Ground truth for an entry with `directory` names files relative to that directory (redis, sentry-browser, next, typescript do), not to the repository: bcgov (app) and HexaRot (backend). No site, rule or line changes. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../src/packs/prisma-client/ground-truth.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/packages/core/src/packs/prisma-client/ground-truth.json b/packages/core/src/packs/prisma-client/ground-truth.json index 4c44045..27bfdb4 100644 --- a/packages/core/src/packs/prisma-client/ground-truth.json +++ b/packages/core/src/packs/prisma-client/ground-truth.json @@ -96,16 +96,16 @@ "with": { "prisma": "7.10.0" }, "why": "An Express API: datasourceUrl becomes the pg adapter, connection_limit moves from the URL to the pool's max, the schema path leaves package.json. It also switched to the prisma-client generator; the Prisma.dmmf, Prisma.validator and import rewrites follow that switch (prisma-client-js 7 still has them), and the reporting SQL changes are unrelated.", "findings": [ - { "file": "app/src/db/prisma/schema.prisma", "line": 2, "rule": "client-js-generator" }, - { "file": "app/src/db/prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, - { "file": "app/package.json", "line": 132, "rule": "package-json-prisma" }, + { "file": "src/db/prisma/schema.prisma", "line": 2, "rule": "client-js-generator" }, + { "file": "src/db/prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, + { "file": "package.json", "line": 132, "rule": "package-json-prisma" }, { - "file": "app/src/db/database.ts", + "file": "src/db/database.ts", "line": 21, "rule": "connection-url-params", "note": "&connection_limit= dropped from the URL, max set on the adapter" }, - { "file": "app/src/db/database.ts", "line": 35, "rule": "client-adapter" } + { "file": "src/db/database.ts", "line": 35, "rule": "client-adapter" } ] }, { @@ -162,15 +162,15 @@ "withoutGenerated": "backend/prisma.config.ts reads env(\"DATABASE_URL\"), which throws when the variable is unset: prisma generate needs the project's database URL at this commit.", "why": "A NestJS backend already on prisma-client and prisma.config.ts: engine \"classic\" leaves the config (TS2353 at 7), the seed moves out of package.json, the clients get the pg adapter. PrismaService (extends PrismaClient) gained a constructor: an insertion into a class body, which the ground-truth rules do not count as a site.", "findings": [ - { "file": "backend/prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, + { "file": "prisma/schema.prisma", "line": 9, "rule": "schema-datasource-url" }, { - "file": "backend/package.json", + "file": "package.json", "line": 108, "rule": "package-json-prisma", "note": "line 107 only loses the comma before the removed key" }, - { "file": "backend/prisma.config.ts", "line": 12, "rule": "config-removed-options" }, - { "file": "backend/prisma/seed.ts", "line": 3, "rule": "client-adapter" } + { "file": "prisma.config.ts", "line": 12, "rule": "config-removed-options" }, + { "file": "prisma/seed.ts", "line": 3, "rule": "client-adapter" } ] } ] From 90cc9fee538a4230bfdecef06a7467ce91b25b03 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:25:24 -0700 Subject: [PATCH 5/9] fix(prisma): fold the stale generated client, skip generated output and mocks Linking @prisma/client 7 next to a client generated at 6 gives compile errors no real upgrade has: they fold into one generated-client finding at the schema's generator block, as its evidence. Files a generator writes (its output directory, Prisma's generated header) are not read, a client the file mocks with jest.mock/vi.mock is never a site, and tests are not where a connection pool is configured. prisma-client-js is a decision: 7 still supports it. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../core/src/packs/prisma-client/detect.ts | 36 ++++- .../fixtures/client-adapter-mock/after.ts | 10 +- .../fixtures/client-adapter-mock/before.ts | 10 +- .../prisma-client/fixtures/schema/after.ts | 4 +- .../prisma-client/fixtures/schema/before.ts | 4 +- .../core/src/packs/prisma-client/index.ts | 137 ++++++++++++++++-- 6 files changed, 173 insertions(+), 28 deletions(-) diff --git a/packages/core/src/packs/prisma-client/detect.ts b/packages/core/src/packs/prisma-client/detect.ts index 03dc050..5d81ee5 100644 --- a/packages/core/src/packs/prisma-client/detect.ts +++ b/packages/core/src/packs/prisma-client/detect.ts @@ -92,6 +92,22 @@ export function schemaEnvReads(text: string): { line: number; name: string }[] { }); } +/** Each generator's `output` and the `generator x {` line, as the schema writes them. */ +export function generators(text: string): { line: number; snippet: string; output?: string }[] { + const out: { line: number; snippet: string; output?: string }[] = []; + const lines = text.split('\n'); + lines.forEach((raw, i) => { + if (/^\s*generator\s+\w+\s*\{/.test(raw)) out.push({ line: i + 1, snippet: raw.trim() }); + }); + for (const p of schemaProperties(text)) + if (p.block === 'generator' && p.key === 'output') { + const owner = [...out].reverse().find((g) => g.line < p.line); + const value = /^"([^"]+)"$/.exec(p.value)?.[1]; + if (owner && value) owner.output = value; + } + return out; +} + /** Enum members with `@map`: their runtime value changed in 7.0.0–7.2.x only. */ export function mappedEnumMembers(text: string): number[] { const lines: number[] = []; @@ -319,14 +335,23 @@ function loadedModule(source: ts.SourceFile, node: ts.Expression): string | unde return isRequire || isImport ? specifierText(source, e.arguments[0]) : undefined; } -/** Local names bound to a Prisma `PrismaClient` class: imported, required or dynamically imported. */ +/** + * Local names bound to a Prisma `PrismaClient` class: imported, required or dynamically imported. + * A module the file mocks (`jest.mock('@prisma/client', …)`, `vi.mock`) gives a fake: never a site. + */ function clientNames(source: ts.SourceFile): Set { const names = new Set(); + const mocked = new Set( + [...source.text.matchAll(/\b(?:jest|vi)\.(?:do)?[mM]ock\(\s*['"]([^'"]+)['"]/g)].map( + (m) => m[1], + ), + ); + const client = (specifier: string) => CLIENT_MODULE.test(specifier) && !mocked.has(specifier); walk(source, (node) => { if ( ts.isImportDeclaration(node) && ts.isStringLiteral(node.moduleSpecifier) && - CLIENT_MODULE.test(node.moduleSpecifier.text) + client(node.moduleSpecifier.text) ) { const bindings = node.importClause?.namedBindings; if (bindings && ts.isNamedImports(bindings)) @@ -339,7 +364,7 @@ function clientNames(source: ts.SourceFile): Set { ts.isObjectBindingPattern(node.name) ) { const from = loadedModule(source, node.initializer); - if (from === undefined || !CLIENT_MODULE.test(from)) return; + if (from === undefined || !client(from)) return; for (const el of node.name.elements) { const imported = el.propertyName ?? el.name; if ( @@ -487,8 +512,11 @@ export function adapterRenameSites(text: string, file: string): SourceSite[] { const URL_PARAM = /[?&](?:connection_limit|pool_timeout|connect_timeout|max_idle_connection_lifetime|max_connection_lifetime)=/; +/** A test asserts a URL; the app configures its pool elsewhere. */ +const TEST_FILE = /(?:^|\/)(?:__tests__|tests?)\/|\.(?:test|spec)\.[cm]?[jt]sx?$/; + export function urlParamSites(text: string, file: string): SourceSite[] { - if (!SOURCE.test(file) || !URL_PARAM.test(text)) return []; + if (!SOURCE.test(file) || TEST_FILE.test(file) || !URL_PARAM.test(text)) return []; return text.split('\n').flatMap((line, i) => { const at = line.search(URL_PARAM); return at >= 0 && !/^\s*(?:\/\/|\*)/.test(line) ? [site(i + 1, at + 1, line)] : []; diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts index 50762ad..ced73c8 100644 --- a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/after.ts @@ -1,3 +1,9 @@ -// A jest mock of the client: its PrismaClient is a local function, not Prisma's. -export const PrismaClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +import { PrismaClient } from '@prisma/client'; + +// A jest mock of the client: its PrismaClient is a fake, never Prisma's. +jest.mock('@prisma/client', () => ({ PrismaClient: jest.fn(() => ({})) })); + export const mockPrisma = new PrismaClient(); // @uptide client-adapter keep + +export const LocalClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +export const local = new LocalClient(); // @uptide client-adapter keep diff --git a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts index 50762ad..ced73c8 100644 --- a/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts +++ b/packages/core/src/packs/prisma-client/fixtures/client-adapter-mock/before.ts @@ -1,3 +1,9 @@ -// A jest mock of the client: its PrismaClient is a local function, not Prisma's. -export const PrismaClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +import { PrismaClient } from '@prisma/client'; + +// A jest mock of the client: its PrismaClient is a fake, never Prisma's. +jest.mock('@prisma/client', () => ({ PrismaClient: jest.fn(() => ({})) })); + export const mockPrisma = new PrismaClient(); // @uptide client-adapter keep + +export const LocalClient = jest.fn().mockImplementation(() => ({ $disconnect: jest.fn() })); +export const local = new LocalClient(); // @uptide client-adapter keep diff --git a/packages/core/src/packs/prisma-client/fixtures/schema/after.ts b/packages/core/src/packs/prisma-client/fixtures/schema/after.ts index 97b348f..24dbc8e 100644 --- a/packages/core/src/packs/prisma-client/fixtures/schema/after.ts +++ b/packages/core/src/packs/prisma-client/fixtures/schema/after.ts @@ -1,11 +1,11 @@ generator client { - provider = "prisma-client-js" // @uptide client-js-generator + provider = "prisma-client-js" engineType = "binary" // @uptide engine-type previewFeatures = ["metrics", "views"] // @uptide metrics-removed } generator docs { - provider = "prisma-docs-generator" // @uptide client-js-generator keep + provider = "prisma-docs-generator" } datasource db { diff --git a/packages/core/src/packs/prisma-client/fixtures/schema/before.ts b/packages/core/src/packs/prisma-client/fixtures/schema/before.ts index 97b348f..24dbc8e 100644 --- a/packages/core/src/packs/prisma-client/fixtures/schema/before.ts +++ b/packages/core/src/packs/prisma-client/fixtures/schema/before.ts @@ -1,11 +1,11 @@ generator client { - provider = "prisma-client-js" // @uptide client-js-generator + provider = "prisma-client-js" engineType = "binary" // @uptide engine-type previewFeatures = ["metrics", "views"] // @uptide metrics-removed } generator docs { - provider = "prisma-docs-generator" // @uptide client-js-generator keep + provider = "prisma-docs-generator" } datasource db { diff --git a/packages/core/src/packs/prisma-client/index.ts b/packages/core/src/packs/prisma-client/index.ts index 27cf8cd..50c0eae 100644 --- a/packages/core/src/packs/prisma-client/index.ts +++ b/packages/core/src/packs/prisma-client/index.ts @@ -1,7 +1,10 @@ -import { join, resolve } from 'node:path'; +import { readFileSync } from 'node:fs'; +import { join, posix, resolve } from 'node:path'; import { projectFiles } from '../../check/importers.js'; +import type { Finding } from '../../domain/report.js'; import { minVersion, satisfies, validRange } from '../../fetch/range.js'; import { type BehaviorNote, definePack, detected, type Pack, type PackRule } from '../contract.js'; +import { packInventory } from '../inventory.js'; import type { PackContext } from '../types.js'; import { ADAPTER_RENAMES, @@ -13,6 +16,7 @@ import { datasourceUrlSites, engineTypeSites, envNotLoadedSites, + generators, implicitStepSites, mappedEnumMembers, metricsSites, @@ -167,17 +171,6 @@ const rules: PackRule[] = [ guide: '`@prisma/client/runtime/library` is `@prisma/client/runtime/client` (already exported by 6.19): `Decimal`, `PrismaClientKnownRequestError`, `JsonValue`, `Sql` and the other public names are there; the Decimal.js internals, `Metric*` and `objectEnumValues` are not. `@prisma/client/wasm` is `@prisma/client/edge` in 7 (`/edge` now means what `/wasm` did). React Native and the binary engine are gone.', }, - { - id: 'client-js-generator', - summary: - 'the `prisma-client-js` generator is deprecated in 7: `prisma-client` generates the client into the project, with an `output` path', - severity: 'deprecated', - kinds: [], - symbols: /$^/, - detect: clientJsSites, - guide: - '`provider = "prisma-client"` with `output = "../src/generated/prisma"` (any path in the project), then import `PrismaClient` and `Prisma` from `/client` instead of `@prisma/client`. Its output is ESM unless `moduleFormat = "cjs"`. It does not generate `Prisma.dmmf` or `Prisma.validator`. 7 still supports `prisma-client-js`: switch only when the owner wants to.', - }, { id: 'engine-type', summary: @@ -224,6 +217,27 @@ function belowMinimums(context: PackContext): string[] { } const behavior: BehaviorNote[] = [ + { + id: 'client-js-generator', + summary: + 'the `prisma-client-js` generator is deprecated in 7 (still supported): `prisma-client` generates the client into the project with an `output` path, so every import of `@prisma/client` moves to `/client`; its output is ESM unless `moduleFormat = "cjs"`, and it has no `Prisma.dmmf` or `Prisma.validator`', + reported: ['decision'], + applies: (context) => { + const sites = schemas(context).flatMap(({ file, text }) => + clientJsSites(text, file).map((s) => `\`${file}:${s.line}\``), + ); + return sites.length + ? `\`prisma-client-js\` is deprecated in 7 but still works: ${sites.join(', ')}. Switching to \`prisma-client\` moves the generated client into the project and every import of \`@prisma/client\` with it; it is the owner's call, not part of this upgrade` + : false; + }, + }, + { + id: 'generated-client', + summary: + "the client was generated by Prisma 6: compile errors against it at 7 are its own, not the code's. Once the schema is valid for 7, `prisma generate` writes the 7 client, and the code is checked against that", + reported: ['finding'], + severity: 'deprecated', + }, { id: 'env-not-loaded', summary: @@ -370,15 +384,106 @@ export const prismaClientPack: Pack = { runtimeFindings(input) { const inventory = input.files ?? []; const configured = inventory.some((f) => CONFIG.test(f)); + const read = (file: string) => input.read(join(input.workspace, file)); + const generated = generatedDirs(inventory, read); const sources = projectFiles(resolve(input.root, input.workspace)).files; - const all = [...new Set([...input.usages.map((u) => u.file), ...inventory, ...sources])].sort(); - return all.flatMap((file) => { - const text = input.read(join(input.workspace, file)); - if (text === undefined) return []; + const all = [...new Set([...input.usages.map((u) => u.file), ...inventory, ...sources])]; + return all.sort().flatMap((file) => { + if (generated.some((dir) => file.startsWith(`${dir}/`))) return []; + const text = read(file); + if (text === undefined || GENERATED_HEADER.test(text)) return []; if (/\.[cm]?[jt]sx?$/.test(file) && !CONFIG.test(file) && !MENTIONS.test(text)) return []; return detected(base, { file, text, from: input.from, to: input.to }).filter( (f) => !(configured && f.rule === 'env-not-loaded'), ); }); }, + foldFindings: (input) => foldStaleClient(input), }; + +/** The first line of every file `prisma generate` writes with the `prisma-client` generator. */ +const GENERATED_HEADER = /^\/\* !!! This is code generated by Prisma/; + +/** Workspace-relative directories a schema's generators write to (`prisma-client` `output`). */ +function generatedDirs(inventory: readonly string[], read: (file: string) => string | undefined) { + return inventory.flatMap((file) => + file.endsWith('.prisma') + ? generators(read(file) ?? '').flatMap((g) => { + const dir = g.output && posix.normalize(posix.join(posix.dirname(file), g.output)); + return dir && !dir.startsWith('..') && !dir.includes('node_modules') ? [dir] : []; + }) + : [], + ); +} + +/** + * `check` compiles against the client the installed CLI generated, next to `@prisma/client` 7: + * the errors no rule claims there (`has no exported member 'PrismaClient'`, implicit anys where + * the generated types fell back to `any`) are that stale client, which `prisma generate` at 7 + * replaces. They fold into one `generated-client` finding at the schema's generator block, as + * its evidence, and stop being sites of their own. + */ +function foldStaleClient(input: { + root: string; + workspace: string; + findings: Finding[]; +}): Finding[] { + const stale = input.findings.filter( + (f) => f.rule === undefined && !f.anchorOnly && base.ruleOf?.(f) === undefined, + ); + if (stale.length === 0) return input.findings; + const dir = resolve(input.root, input.workspace); + const read = (file: string) => { + try { + return readFileSync(join(dir, file), 'utf8'); + } catch { + return undefined; + } + }; + const inventory = packInventory(input.root, input.workspace, ['**/*.prisma']); + const generated = generatedDirs(inventory, read); + const anchor = inventory + .filter((file) => !generated.some((g) => file.startsWith(`${g}/`))) + .flatMap((file) => generators(read(file) ?? '').map((g) => ({ file, ...g })))[0]; + if (!anchor) return input.findings; + const first = stale[0] as Finding; + const { compileError: _error, compileCode: _code, ...usage } = first.usage; + const path = '@prisma/client:generated-client'; + const folded: Finding = { + ...first, + change: { + ...first.change, + path, + kind: 'deprecated', + severity: 'deprecated', + source: 'pack', + confidence: 1, + }, + usage: { + ...usage, + file: anchor.file, + line: anchor.line, + column: 1, + endLine: anchor.line, + endColumn: 1, + symbolPath: path, + snippet: anchor.snippet, + via: 'inferred', + }, + severity: 'deprecated', + confidence: 1, + fixability: 'manual', + rule: 'generated-client', + anchorOnly: true, + downstream: stale.map((e) => ({ + file: e.usage.file, + line: e.usage.line, + column: e.usage.column, + snippet: e.usage.snippet, + code: e.usage.compileCode ?? Number(/^TS(\d+)$/.exec(e.change.path)?.[1] ?? 0), + message: e.usage.compileError ?? e.reason, + })), + reason: `${stale.length} compile error${stale.length === 1 ? '' : 's'} against the client Prisma 6 generated: \`prisma generate\` at 7 replaces it`, + }; + return [...input.findings.filter((f) => !stale.includes(f)), folded]; +} From 9e25a4544182af6dcdd0c32bd50f296d7f801017 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:27:58 -0700 Subject: [PATCH 6/9] fix(prisma): fold a workspace's stale client at its @prisma/client line A workspace whose schema lives at the monorepo root (cr-league apps/api) has no schema of its own to anchor the generated-client finding: its package.json line that declares @prisma/client is where its client comes from. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- packages/core/src/packs/prisma-client/index.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/core/src/packs/prisma-client/index.ts b/packages/core/src/packs/prisma-client/index.ts index 50c0eae..19c3a27 100644 --- a/packages/core/src/packs/prisma-client/index.ts +++ b/packages/core/src/packs/prisma-client/index.ts @@ -442,9 +442,19 @@ function foldStaleClient(input: { }; const inventory = packInventory(input.root, input.workspace, ['**/*.prisma']); const generated = generatedDirs(inventory, read); - const anchor = inventory - .filter((file) => !generated.some((g) => file.startsWith(`${g}/`))) - .flatMap((file) => generators(read(file) ?? '').map((g) => ({ file, ...g })))[0]; + // The schema's generator block; a workspace whose schema lives elsewhere (a monorepo's root) + // gets its client from the `@prisma/client` line of its package.json. + const anchor = + inventory + .filter((file) => !generated.some((g) => file.startsWith(`${g}/`))) + .flatMap((file) => generators(read(file) ?? '').map((g) => ({ file, ...g })))[0] ?? + (read('package.json') ?? '') + .split('\n') + .flatMap((text, i) => + /^\s*"@prisma\/client"\s*:/.test(text) + ? [{ file: 'package.json', line: i + 1, snippet: text.trim() }] + : [], + )[0]; if (!anchor) return input.findings; const first = stale[0] as Finding; const { compileError: _error, compileCode: _code, ...usage } = first.usage; From 2aff5a917bf008b25967fae939ca2e6b8bfd5267 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:36:12 -0700 Subject: [PATCH 7/9] fix(prisma): list a decision only where the repository has its sites cli-implicit-steps names the package.json scripts it found; the connection URL parameters are findings only, since the decision context holds no source strings. A jqlang smoke run listed both with nothing behind them. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- packages/core/src/packs/prisma-client/index.ts | 11 ++++++++++- .../src/packs/prisma-client/prisma-client.test.ts | 9 +++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/packages/core/src/packs/prisma-client/index.ts b/packages/core/src/packs/prisma-client/index.ts index 19c3a27..5870f20 100644 --- a/packages/core/src/packs/prisma-client/index.ts +++ b/packages/core/src/packs/prisma-client/index.ts @@ -261,12 +261,21 @@ const behavior: BehaviorNote[] = [ reported: ['finding', 'decision'], severity: 'deprecated', detect: implicitStepSites, + applies: (context) => { + const sites = files(context).flatMap(({ file, text }) => + implicitStepSites(text, file).map((s) => `\`${file}:${s.line}\` (${s.name})`), + ); + return sites.length + ? `\`prisma migrate dev\`/\`migrate reset\` no longer seed and \`migrate dev\`/\`db push\` no longer generate in 7: ${sites.join(', ')}. Add \`prisma generate\` or \`prisma db seed\` where the workflow relied on them` + : false; + }, }, { id: 'connection-url-params', summary: 'the v6 connection URL parameters `connection_limit`, `pool_timeout`, `connect_timeout`, `max_idle_connection_lifetime` and `max_connection_lifetime` are not read by the 7 driver adapters: set the pool on the adapter (`max`, `connectionTimeoutMillis`, `idleTimeoutMillis` for pg), whose defaults differ (pg: no connection timeout, where 6 had 5 s)', - reported: ['finding', 'decision'], + // Its sites are source strings the decision context does not hold: the finding names them. + reported: ['finding'], severity: 'deprecated', detect: urlParamSites, }, diff --git a/packages/core/src/packs/prisma-client/prisma-client.test.ts b/packages/core/src/packs/prisma-client/prisma-client.test.ts index 47f8582..e537494 100644 --- a/packages/core/src/packs/prisma-client/prisma-client.test.ts +++ b/packages/core/src/packs/prisma-client/prisma-client.test.ts @@ -86,6 +86,15 @@ describe('decisions', () => { ); }); + it('lists the scripts that relied on migrate dev generating or seeding, and nothing without one', () => { + const manifest = + '{\n "scripts": {\n "db:dev": "prisma migrate dev",\n "build": "next build"\n }\n}\n'; + expect(decide('cli-implicit-steps', '7.10.1', { 'package.json': manifest })).toMatch( + /`package.json:3` \(db:dev\)/, + ); + expect(decide('cli-implicit-steps', '7.10.1', { 'package.json': '{}' })).toBe(false); + }); + it('warns about mapped enums only for a target that changes them', () => { expect(decide('mapped-enum-values', '7.2.0', { 'prisma/schema.prisma': schema })).toMatch( /prisma\/schema.prisma:7/, From 70a7d862042c64ddc089d4a52abfbcc0847d12cf Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:36:12 -0700 Subject: [PATCH 8/9] test(prisma): record what pack test measured uptide pack test @prisma/client --write: 7 public repositories, 29 of 29 breaking sites found, no breaking false positive (verified gate); cli-skip-flags exercised by basalt. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .../src/packs/prisma-client/verification.json | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/core/src/packs/prisma-client/verification.json b/packages/core/src/packs/prisma-client/verification.json index bfe31eb..8bfaf39 100644 --- a/packages/core/src/packs/prisma-client/verification.json +++ b/packages/core/src/packs/prisma-client/verification.json @@ -1,31 +1,31 @@ { - "status": "candidate", - "truth": "sha256-b16bb874a2ced0eaa8a7a537655bcfb5f8584e406087a4234401ab5df9c1b43f", - "repos": 0, + "status": "verified", + "truth": "sha256-9ae79f811388f21d660fe6ae1cb72bb7b9e766cfd99f0d10340668489a98de25", + "repos": 7, "breaking": { "precision": 1, "recall": 1, "falsePositives": 0, - "truePositives": 0, + "truePositives": 29, "falseNegatives": 0 }, "behavior": { - "precision": 1, - "recall": 1, - "falsePositives": 0, - "truePositives": 0, - "falseNegatives": 0 + "precision": 0.25, + "recall": 0.6, + "falsePositives": 9, + "truePositives": 3, + "falseNegatives": 2 }, "class": "breaking", - "breakingSites": 0, - "breakingFoundAny": 0, - "breakingFound": 0, - "breakingScored": 0, - "behaviorSites": 0, - "unexercised": ["example-rename"], + "breakingSites": 29, + "breakingFoundAny": 29, + "breakingFound": 29, + "breakingScored": 29, + "behaviorSites": 6, + "unexercised": [], "mechanical": { - "example-rename": { - "truePositives": 0, + "cli-skip-flags": { + "truePositives": 1, "falsePositives": 0 } } From 2569d8af7f5664bdf0c818dfaef11f88484f7e94 Mon Sep 17 00:00:00 2001 From: Lucas Braum Date: Tue, 20 Oct 2026 11:39:47 -0700 Subject: [PATCH 9/9] docs(prisma): ground truth, measured score and gaps; tables, queue and changeset RESEARCH.md records every repository screened, the measured pack test and what the engine cannot express. pnpm docs:packs adds the pack to the README and the compatibility matrix; the queue entry is verified. Co-Authored-By: Claude Opus 5.5 Signed-off-by: Lucas Braum --- .changeset/prisma-six-to-seven-pack.md | 5 + README.md | 1 + docs/compatibility.md | 2 + .../core/src/packs/prisma-client/RESEARCH.md | 133 ++++++++++++++++-- packs/queue.json | 2 +- 5 files changed, 131 insertions(+), 12 deletions(-) create mode 100644 .changeset/prisma-six-to-seven-pack.md diff --git a/.changeset/prisma-six-to-seven-pack.md b/.changeset/prisma-six-to-seven-pack.md new file mode 100644 index 0000000..64cccec --- /dev/null +++ b/.changeset/prisma-six-to-seven-pack.md @@ -0,0 +1,5 @@ +--- +"uptide": minor +--- + +Add a verified @prisma/client 6 to 7 migration pack. `check` finds what Prisma 7 rejects in the schema (`url`, `directUrl`, `shadowDatabaseUrl`, the metrics preview feature, MongoDB), in package.json (the `"prisma"` key, removed CLI flags and commands) and in `prisma.config.ts` (`engine`, `adapter`), and every `new PrismaClient()` that has no driver adapter, including clients a test or script loads with `require` or a dynamic import. `prisma` moves with `@prisma/client`, and so does each installed `@prisma/adapter-*`. `fix` drops `--skip-generate` and `--skip-seed` itself, since 7 never generates or seeds there; the adapter, the schema URL and the config move are assisted. The .env loading change, the `migrate dev` steps that no longer run, v6 connection URL pool parameters, SSL validation and the Node and TypeScript minimums are listed for the owner. Compile errors from the client Prisma 6 generated are folded into one finding at the schema's generator block instead of being reported as breaking sites. diff --git a/README.md b/README.md index 01e075d..26bf553 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,7 @@ See the [pack compatibility matrix](docs/compatibility.md) and the [migration pa | `express` | 4 → 5 | 8 | 100% | 86% | 86% | 86 | netlify/cli, bcgov/tenant-management-system, amiantos/lurker, openhab/openhab-cloud | verified | | `lucide-react` | 0 → 1 | 5 | 100% | 100% | 100% | 11 | tradeblocks-org/tradeblocks, spizeck/SFPCA, cubicsteak/keep | verified | | `next` | 15 → 16 | 9 | 100% | 88% | 88% | 34 | sambecker/exif-photo-blog, stoczec/wortkarte, benshabbat/GamesForMyKids, HiroshiIgarash/koremina | verified | +| `@prisma/client` | 6 → 7 | 12 | 100% | 100% | 100% | 29 | BearStudio/start-ui-web, jqlang/playground, openwallet-foundation-labs/ehr-wallet, bcgov/nr-permitconnect-navigator-service, basaltkit/basalt, AlexAgo83/cr-league, MarvinLeRouge/HexaRot | verified | | `react` | 18 → 19 | 8 | 100% | 95% | 95% | 130 | excalidraw/excalidraw, tldraw/tldraw | verified | | `react-router` | 6 → 7 | 13 | 100% | 89% | 78% | 18 | Arize-ai/phoenix, getlago/lago-front | verified | | `redis` | 4.x/5.x → 5.x/6.x | 14 | 100% | 85% | 75% | 20 | deanmoses/tacocat-gallery-sam, swoofer/mcp-coordinator, UNN-Devotek/FCM-Fallout-Chat-Mod, tearingItUp786/blog | verified | diff --git a/docs/compatibility.md b/docs/compatibility.md index b43d77c..5b0e894 100644 --- a/docs/compatibility.md +++ b/docs/compatibility.md @@ -37,6 +37,7 @@ already run as assisted ([verification gate](packs.md)). | `lucide-react` | 0 → 1 | 5 | 100% | 100% | 100% | 11 | tradeblocks-org/tradeblocks, spizeck/SFPCA, cubicsteak/keep | verified | | `next` | 15 → 16 | 9 | 100% | 88% | 88% | 34 | sambecker/exif-photo-blog, stoczec/wortkarte, benshabbat/GamesForMyKids, HiroshiIgarash/koremina | verified | | `openai` | 4 → 7 | 15 | 100% | — | — | 20 | zeikar/charivo, openai/openai-guardrails-js, wardby/wardby, csnyder256/ux-struggle-detector, grantsingleton/batch-ai, digitalchokro/askchokro, microsoft/prompty | candidate | +| `@prisma/client` | 6 → 7 | 12 | 100% | 100% | 100% | 29 | BearStudio/start-ui-web, jqlang/playground, openwallet-foundation-labs/ehr-wallet, bcgov/nr-permitconnect-navigator-service, basaltkit/basalt, AlexAgo83/cr-league, MarvinLeRouge/HexaRot | verified | | `react` | 18 → 19 | 8 | 100% | 95% | 95% | 130 | excalidraw/excalidraw, tldraw/tldraw | verified | | `react-router` | 6 → 7 | 13 | 100% | 89% | 78% | 18 | Arize-ai/phoenix, getlago/lago-front | verified | | `redis` | 4.x/5.x → 5.x/6.x | 14 | 100% | 85% | 75% | 20 | deanmoses/tacocat-gallery-sam, swoofer/mcp-coordinator, UNN-Devotek/FCM-Fallout-Chat-Mod, tearingItUp786/blog | verified | @@ -70,6 +71,7 @@ already run as assisted ([verification gate](packs.md)). | `openai` | `removed-modules` | n/a | 0 | 0 | unexercised: runs assisted | | `openai` | `request-options` | n/a | 0 | 0 | unexercised: runs assisted | | `openai` | `runner-renames` | n/a | 0 | 0 | unexercised: runs assisted | +| `@prisma/client` | `cli-skip-flags` | 100% | 1 | 0 | | | `react` | `mutable-ref-object` | 100% | 2 | 0 | | | `react` | `ref-callback-return` | 100% | 1 | 0 | | | `react` | `use-ref-argument` | 100% | 5 | 0 | | diff --git a/packages/core/src/packs/prisma-client/RESEARCH.md b/packages/core/src/packs/prisma-client/RESEARCH.md index 6a260b4..cd6b7cb 100644 --- a/packages/core/src/packs/prisma-client/RESEARCH.md +++ b/packages/core/src/packs/prisma-client/RESEARCH.md @@ -35,7 +35,7 @@ constructor options) is **generated** by the `prisma` CLI into `node_modules/.pr client gives diagnostics no real upgrade has: `packs:truth` reports TS2305 `has no exported member 'PrismaClient'` at every `import … from '@prisma/client'` (a linked copy no longer reaches `.prisma/client`), and TS7006/TS7031/TS2339 wherever the generated types fell back to `any` - (bcgov: 130 such diagnostics). A real 7 install runs `prisma generate` at 7, after which none of + (bcgov: 570 such diagnostics across its two tsconfigs). A real 7 install runs `prisma generate` at 7, after which none of them exist. They are never ground truth (see below). - The compiler-visible 7 changes (TS2554/TS2345 on a constructor without `adapter`, TS2353 on `datasources`/`datasourceUrl`, TS2339 on `$use`/`$metrics`, TS2694 on `Prisma.Middleware`, all @@ -72,8 +72,8 @@ constructor options) is **generated** by the `prisma` CLI into `node_modules/.pr ## Classified changes "Compiler" = a typed consumer gets a diagnostic once the client is generated at 7 (probe X). -"Detect" = how this pack finds the site. Every rule here is assisted (no `rewrite`); the last -column says why. +"Detect" = how this pack finds the site. Every rule here is assisted except `cli-skip-flags`; the +last column says why. | Change | Source | Compiler | Where | Rule / note (`reported`) | Why assisted | |---|---|---|---|---|---| @@ -83,10 +83,11 @@ column says why. | `datasource.url`, `directUrl`, `shadowDatabaseUrl` not allowed in the schema; `prisma.config.ts` carries the URL | G, R0, C (P1012 "is no longer supported in schema files", probe `prisma validate`) | n/a (schema error) | schema.prisma | `schema-datasource-url` (breaking), detect | the URL moves to `prisma.config.ts` (create or edit, `env()` vs `process.env`, `directUrl` becomes `url`) | | `.env` no longer loaded: by the CLI (6 loaded it unless `prisma.config.ts` existed, P) and by the client at runtime (D6: `runtime/library.js` loads `relativeEnvPaths` in the constructor when no adapter is passed; nothing in D7) | G, R0, P, D6 | no | package.json scripts; schema `env("…")` | `env-not-loaded` (finding + decision, needs a look), detect on scripts that run a database command of the CLI without loading an env file; the decision names the schema's `env()` reads, where the app read the URL on 6 | behavior: whether the environment provides the variable is the owner's knowledge | | `migrate dev`/`migrate reset` no longer seed; `migrate dev`/`db push` no longer generate; no postinstall generate | G, R0 | no | package.json scripts | `cli-implicit-steps` (finding + decision, needs a look), detect | behavior | -| `--skip-generate`, `--skip-seed`, `db execute --schema/--url`, `migrate diff --from-url/--to-url/--from-schema-datasource/--to-schema-datasource/--shadow-database-url/--from-schema-datamodel/--to-schema-datamodel/--from-local-d1/--to-local-d1`, `generate --data-proxy/--accelerate/--no-engine/--allow-no-models`, `db pull --local-d1`, `prisma introspect`, removed `PRISMA_*` variables | G, R0 | no | package.json scripts | `cli-removed-flags` (breaking), detect | each has its own replacement (config datasource, explicit `prisma generate`); `--skip-generate` alone could be dropped 1:1, but no real site exercises it in a package.json script (basalt's is one), see "Rewrites" | +| `--skip-generate` (`migrate dev`, `db push`), `--skip-seed` (`migrate dev`) | G, R0 | no | package.json scripts | `cli-skip-flags` (breaking), detect, **rewrite** | not assisted: 7 neither generates nor seeds there, which is what the flags turned off, so dropping them is the same command; the rewrite declines when a skip flag belongs to another command (fixture `scripts`, `test` line) | +| `db execute --schema/--url`, `migrate diff --from-url/--to-url/--from-schema-datasource/--to-schema-datasource/--shadow-database-url/--from-schema-datamodel/--to-schema-datamodel/--from-local-d1/--to-local-d1`, `generate --data-proxy/--accelerate/--no-engine/--allow-no-models`, `db pull --local-d1`, `prisma introspect`, removed `PRISMA_*` variables | G, R0 | no | package.json scripts | `cli-removed-flags` (breaking), detect | each has its own replacement (`--from-config-datasource`, the config's shadow database, `db pull`) | | `"prisma"` key in package.json (`schema`, `seed`) removed | R0 | no | package.json | `package-json-prisma` (breaking), detect | moves to `prisma.config.ts` (`schema`, `migrations.seed`) | | `prisma.config.ts` `engine` and `adapter` options removed | R0 | `prisma` config types only | prisma.config.* | `config-removed-options` (breaking), detect | `engine: 'classic'` is simply dropped, but `adapter` moves to the client | -| `prisma-client-js` deprecated for `prisma-client` (generated code in the repository, imports change) | G, R0, C ("Legacy Prisma Client JS generator") | no | schema.prisma | `client-js-generator` (deprecated), detect | changes every import, the output path, ESM output (`moduleFormat`) and some generated APIs (`Prisma.dmmf`, `Prisma.validator`) | +| `prisma-client-js` deprecated for `prisma-client` (generated code in the repository, imports change) | G, R0, C ("Legacy Prisma Client JS generator") | no | schema.prisma | `client-js-generator` (decision, names the provider lines) | 7 still supports it (X), so switching is the owner's choice, not a site: it changes every import, the output path, ESM output (`moduleFormat`) and some generated APIs (`Prisma.dmmf`, `Prisma.validator`). It was a deprecated rule with `detect` until the first score: 2 of the 5 repositories on `prisma-client-js` switched, so it reported a site the upgrade did not need in the other three | | `output` required | G, R0 | n/a | schema.prisma | **rejected as a 6 → 7 change**: C shows 6.19.3 already refuses `prisma-client` without `output`, and 7.10.1 `prisma-client-js` still generates into `node_modules` (X) | n/a | | `engineType` | G, R0 | no | schema.prisma | `engine-type` (deprecated), detect: X shows 7.10.1 still accepts `engineType = "binary"` and ignores it | deleting the line is 1:1, but no real site exists; see "Rewrites" | | ESM (`"type": "module"`, tsconfig `bundler`) | G | no | package.json, tsconfig | **rejected as required**: D7 has no `"type": "module"` and ships CJS `default.js`; `prisma-client` takes `moduleFormat`; X compiled 7 under CommonJS `nodenext`. Named in `client-js-generator`'s guide | n/a | @@ -99,15 +100,16 @@ column says why. | Connection pool and timeouts from the driver; v6 URL parameters (`connection_limit`, `pool_timeout`, `connect_timeout`, `max_idle_connection_lifetime`, `max_connection_lifetime`) not read by the adapters | G, P | no | TS (connection strings), env | `connection-url-params` (finding + decision, needs a look), detect on those parameters in source strings | behavior | | SSL certificates validated by `pg` (P1010) | G | no | adapter options | `ssl-validation` (decision, PostgreSQL and CockroachDB schemas only) | behavior | | Mapped enum values: 7.0.0–7.2.x generate the `@map` value; 7.3.0 reverted to 6 | R0, R (7.3.0), G | no | schema.prisma | `mapped-enum-values` (decision, only for a target below 7.3.0 and a schema with a mapped enum member) | behavior | +| The client `check` compiles against was generated at 6: with `@prisma/client` 7 linked, TS2305 (`has no exported member 'PrismaClient'`), TS7006/TS7031 and TS2339 appear wherever the generated types fell back to `any` | D6, D7, X | yes, but not the code's | TS | `generated-client` (finding, needs a look): `foldFindings` folds every compile error no rule claims into one finding at the schema's `generator` block (or the workspace's `@prisma/client` line when the schema lives outside it), as its evidence | `prisma generate` at 7 replaces the client; the code is then compiled against 7's types (`fix` regenerates after the bump) | | Removed client engines, `binaryTargets`, `generator.runtime = "react-native"`, `prisma introspect` | R0 | no | schema.prisma | none (`binaryTargets` is accepted and unused at 7, probe X); `generic` if ever expected | n/a | ### Rewrites -None. Each candidate that is 1:1 (dropping `--skip-generate`/`--skip-seed`, dropping -`engineType`, `runtime/library` → `runtime/client` when no missing export is imported, an import -of a renamed adapter class) has at most one real site in the ground truth below, and a rewrite with no -real site runs assisted anyway (UPT-48, `unexercised`). So the code would ship and never run. Add -the rewrite together with the first ground-truth repository that exercises it. +One: `cli-skip-flags`, exercised by basaltkit/basalt (`db:push`). The other 1:1 candidates +(dropping `engineType`, `runtime/library` → `runtime/client` when no missing export is imported, +an import of a renamed adapter class) have no real site in the ground truth, and a rewrite with no +real site runs assisted anyway (UPT-48, `unexercised`): the code would ship and never run. Add each +with the first ground-truth repository that exercises it. ## Detection scope @@ -117,9 +119,118 @@ the rewrite together with the first ground-truth repository that exercises it. `$metrics`, a Prisma adapter or a Prisma entry point. `new PrismaClient(...)` counts only where `PrismaClient` is imported or required from `@prisma/client` or from a module path that ends in `/client` or names `prisma`/`generated` (a generated client); a local `PrismaClient` (a jest mock) - is never a site (fixture `client-adapter` line 9). + is never a site, nor is a client the file mocks with `jest.mock`/`vi.mock` (fixture + `client-adapter-mock`). Files a generator writes are never read: a `prisma-client` `output` + directory, a file with Prisma's generated header, a schema copy inside an output. - `schema.prisma` and any `*.prisma` file of a multi-file schema, `package.json`, `prisma.config.*`, `.nvmrc`/`.node-version`: the pack's inventory. - Fixtures for package.json and schema files follow sentry-node and the typescript pack: the file named by `filename.txt`, with `// @uptide` markers (comments in a Prisma schema; ignored by the line-based package.json detectors). + +## Independent ground truth + +Drafted with `pnpm packs:truth` from each repository's own compiler and merged upgrade, `--also +prisma@` and the adapter the upgrade added; every site read by hand and committed +(`cac9bca`) before the first score. `packs:truth` lists changed lines in source files and +tsconfigs only, so the lines the upgrade changed in `schema.prisma`, `package.json` and +`prisma.config.*` were read from the same `git diff -U0 ` with the same rules +(one site per change, at its first line). The compiler draft contributed one site, HexaRot's TS2353 on `engine` in +`prisma.config.ts` (the `prisma` config types at 7); every other new diagnostic was the stale-client +artifact above (bcgov 570 across its two tsconfigs, cr-league 49, ehr-wallet 7, BearStudio 5, +jqlang 1). Kept: the +line the upgrade changed because of Prisma 7. Not kept: lines that only follow the repository's own +fix (basalt's hand-typed `new () =>` signatures, ehr-wallet's `createPrismaClient()` wrapper, the +import and mock rewrites of a generator switch) and cleanup (BearStudio's engine-binary copy plugin, +`@@map` reordering). + +| Repository | Licence | Upgrade | Hop | Sites | +|---|---|---|---|---| +| BearStudio/start-ui-web | MIT | #781 | 6.19.2 → 7.10.0 | schema URL, client adapter, 2 env-not-loaded, 1 cli-implicit-steps | +| jqlang/playground | MIT | #344 | 6.19.3 → 7.8.0 | schema URL, client adapter | +| openwallet-foundation-labs/ehr-wallet | Apache-2.0 | #114 | 6.19.3 → 7.10.0 | schema URL, `"prisma"` key, 7 client adapter, generator switch (decision) | +| bcgov/nr-permitconnect-navigator-service (`app`) | Apache-2.0 | #574 | 6.19.3 → 7.10.0 | schema URL, `"prisma"` key, `datasourceUrl`, `connection_limit`, generator switch (decision) | +| basaltkit/basalt (root; harness in `apps/pg-integration`) | MIT | #188 | 6.19.3 → 7.9.1 | schema URL, `--skip-generate`, 2 client adapter (dynamic import) | +| AlexAgo83/cr-league | MIT | #30 | 6.19.3 → 7.9.1 | schema URL, 3 `datasources`, 1 client adapter | +| MarvinLeRouge/HexaRot (`backend`) | MIT | #160 | 6.19.2 → 7.10.0 | schema URL, `"prisma"` key, config `engine`, client adapter | + +29 breaking sites, 6 behavior-note sites. No commit of the seven carries an agent co-author +trailer, and no PR was opened by a bot account. basalt's PR body has a stray non-ASCII word and +ehr-wallet's commit message is a structured list; both are signed by their human authors and +counted as human. HexaRot is scored `withoutGenerated`: its `prisma.config.ts` reads +`env("DATABASE_URL")`, which throws when the variable is unset. basalt is scored from the root +because `packs:truth` installs in `directory` and its lockfile is at the root. + +### Screened out + +Searched GitHub for merged PRs titled with Prisma 7 upgrades (2026-10-20, about 250 hits; most are +Renovate/Dependabot bumps inside 7 or projects adopting Prisma 7 fresh). Of the 6 → 7 code upgrades +screened (licence, author, trailers, `@prisma/client` range in the diff): + +- Authored with a coding agent (Co-Authored-By trailer or agent in the PR body): kalkulacka-one/kalkulacka#508, soundspan/soundspan#162, CruGlobal/game-ops#171, trentnbauer/QueueUp#1089, goodtribes-org/goodtribes.org#60, jonhadfield/taran#105, Fedbxy/grader#59, kkv2/m4#16, cubitsnake/keep#37, Pavelya/match#14, thomastran117x/Rentify#208, beefysalad/Wanderly#254, IgorCifer/xeque-mate#27, yaniv120892/my-expenses-website#136, umzug-meister/vega#23 (Copilot), augustosc-eu/TunarrTube#19, ecleveland/grimoire-os#338, dangxuanthangqt/ecom#18, nassim0014/Next.js-SaaS#52; and from 5 or earlier: Vivancedata/crm#63, piotrberlowski/mul-search#50 (Cursor), E5GEN2/hbp_proto#228, vohungbk/xiangqi-arena#583, alliso/recipe-aggregator#13, zhedream/blogPrisma#20 (Codex, from Prisma 1). +- No permissive licence: julienfroidefond/stripstream#13, EdenSahile/Mimir#55, 3-Istor/qcm#11, C4G/AtlantaFoodConsortium#33, dragondad22/CrossWise#133 (none), lord007tn/nibleaf#91 (AGPL-3.0), WrongerSandwich/governance-compass#125 (NOASSERTION); mardesnic/blood-donations#61 and Nexlein/Dashboard#121 are also 5 → 7. +- Not a 6 → 7 project upgrade: cerebruminc/typegraphql-prisma#6 (a fork, 4.8 → 7), TriplEight/TaxHacker#61 (a fork, no version change in the PR), Create-Node-App/cna-templates#427 (template files, not a compiled project). +- Human, permissive, not drafted (more evidence for later): openai/openai-agents-js#1887 (an example package: schema URL, `options.client ?? new PrismaClient()`), yuru-sha/fuji-calendar#43, ismoilovdevml/gitlab-ci-dashboard#72 (`--skip-generate` passed as an `execFile` argument, a shape `cli-skip-flags` does not read), Zihao-Qi/Dayflow#148. + +## Measured pack test + +`uptide pack test @prisma/client --write`, 2026-10-20, ground truth as committed: + +``` + rule precision recall tp fp fn + cli-implicit-steps 0% 0% 0 3 1 + cli-skip-flags 100% 100% 1 0 0 + client-adapter 100% 100% 17 0 0 + client-js-generator n/a 0% 0 0 2 + config-removed-options 100% 100% 1 0 0 + connection-url-params 100% 100% 1 0 0 + env-not-loaded 67% 100% 2 1 0 + generated-client 0% n/a 0 5 0 + package-json-prisma 100% 100% 3 0 0 + schema-datasource-url 100% 100% 7 0 0 + overall (sites) 78% 94% 32 9 2 + behavior notes (sites) 25% 60% 3 9 2 + breaking (sites) 100% 100% 29 0 0 + +gate 29 breaking sites, 6 behavior-note sites in public ground truth · verified +rewrites precision tp fp (real true positives, scored false positives) + cli-skip-flags 100% 1 0 +``` + +The 9 false positives are all "needs a look": `generated-client` once per scored workspace (no +upgrade edits the generator block for it; it is where the folded evidence goes), +`cli-implicit-steps` at three `prisma migrate dev` scripts the repositories kept, and +`env-not-loaded` at bcgov's `prisma db pull` script. The two misses are the `client-js-generator` +decisions, which `check` never reports as sites. BearStudio's `db:push` line carries two notes in +the truth and one finding in the plan (one site per line). + +The first score (before `90cc9fe`) had 176 breaking false positives: once the pack's inventory keeps +the analysis going, `check` compiles against the stale client and every TS2305/TS7006 became a +`generic` breaking finding; the generated client's own files were read; ground truth for `directory` +entries named files from the repository root, where every other pack names them from the directory +(`04ac449`, no site changed). + +## Known gaps + +- A class that extends `PrismaClient` without passing options to `super` (NestJS `PrismaService`) + throws at 7 like `new PrismaClient()`. HexaRot fixed one by inserting a constructor: an insertion + into a class body, which the ground-truth rules count as no site, so the pack does not report it + either. Its guide covers it when another site leads the agent there. +- A workspace whose schema lives outside it (cr-league `apps/api`, schema at the root) gets its + schema findings only where the workspace that holds the schema declares `@prisma/client` or is + analyzed for it: the inventory cannot leave a workspace. +- `?schema=` in a PostgreSQL URL: Prisma 6 applied it; `PrismaPg` takes the schema as an option. + cr-league handled it, but no source this pack cites says so, so there is no note. +- `prisma db push` in 7 asks for consent when it detects an AI agent (basalt set + `PRISMA_USER_CONSENT_FOR_DANGEROUS_AI_ACTION` in CI): not in the guide or the release notes read. + +## What the engine cannot express + +- `check` cannot compile against the 7 client without running the target's generator; it runs no + scripts. This pack detects in source what the compiler would report after `prisma generate`, and + folds what it reports before it. Follow-up: generate the target client in an isolated copy. +- `packs:truth` drafts no site in `schema.prisma` or `package.json` (`SOURCE_FILE` in + `packs/tooling/truth-diff.ts`). Those were read by hand from the same diff; adding `*.prisma`, + `package.json` and `prisma.config.*` to its file filter would draft them. +- `packs:truth` and `pack test` install in `directory`, so a monorepo whose lockfile is at the root + is scored from the root (basalt). diff --git a/packs/queue.json b/packs/queue.json index 2dd7d4a..aa9e411 100644 --- a/packs/queue.json +++ b/packs/queue.json @@ -153,7 +153,7 @@ "from": ">=6 <7", "to": ">=7 <8", "why": "declared directly in 14 of 52 sample repositories, 8 of them on an older major; 5261 breaking API changes, 4403 distinct (4403 removed or renamed, 0 change shapes) from 6.19.3 to 7.10.0; upgraded with @prisma/adapter-pg, @prisma/instrumentation (@prisma released in lockstep)", - "status": "todo", + "status": "verified", "repos": 14, "reposBehind": 8, "weeklyDownloads": 0,