| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
feat: an application depends on one Prisma package (ADR 242) (#29864) ## What changes for someone using Prisma Today, an application that talks to Postgres installs a long list of our packages: ```jsonc { "dependencies": { "@prisma-next/postgres": "...", "@prisma-next/sql-runtime": "...", "@prisma-next/sql-orm-client": "...", "@prisma-next/target-postgres": "...", "@prisma-next/adapter-postgres": "...", "@prisma-next/sql-contract": "..." // ...a dozen more } } ``` After this PR, it installs one: ```jsonc { "dependencies": { "@prisma/orm-postgres": "0.16.0" } } ``` Everything else arrives as that package's own dependencies. Three of our example apps are converted in this PR to prove it — one per database — and each has exactly one Prisma package in its `dependencies`. This implements [ADR 242](https://github.com/prisma/prisma/pull/29852), which is already merged. ## What gets published 17 packages, all under the `@prisma` scope: - **3 database packages** — `@prisma/orm-postgres`, `orm-sqlite`, `orm-mongo`. An application installs exactly one. We call these *facades*: each is a small package that wires its database together and re-exports everything an application needs. - **6 extension packs** — PostGIS, pgvector, ParadeDB, Supabase, arktype-json, middleware-cache. Optional, installed alongside a database package. - **7 platform packages** — the framework, the toolchain, one per database family, one per database target. Applications never install these directly; they arrive as dependencies. Extension authors do install them. - **the `prisma` command**, as a bin-only package. Every other workspace package — around 50 of them — stops being published. They still exist in the repo as the unit we organise code in; they just stop having a life on the registry. **This PR does not make that switch yet.** It builds and proves the new surface while leaving today's publish list exactly as it is. Flipping it is a separate change. ## The problem this design has to avoid A published package can't depend on packages that won't exist on the registry. So each published package *contains a compiled copy* of the internal packages it covers. That creates a trap. If one application ends up with the same code twice — once inside a published package, once as its own package — then classes, registries, and anything compared by reference exist twice too. An `instanceof` check quietly returns false. Nothing crashes, nothing fails to compile, and both copies behave identically in isolation. You find out much later, somewhere unrelated. So the rule the whole design follows is: **every piece of internal code is published from exactly one package.** Concretely, that means: - Each published package is built in one pass, so code shared between its own entry points exists once. Verified from the build's source maps: no module appears in more than one chunk, in any published package. - When one published package needs code from another, it imports it as a real dependency rather than compiling in a second copy. - A facade re-exports from the platform packages; it never carries its own copy. `@prisma/orm-postgres/orm-client` and `@prisma/orm-family-sql/orm-client` are two names for the same object, and there's a test that asserts exactly that from installed tarballs. - One table in `packages/0-shared/publish-surface` maps every internal package to where it's published. The build, the code generator, and the lint checks all read it, so there's one answer to "where does this live" rather than three that can drift. ## Generated code follows the application Prisma writes imports into your project — contract types and migration files. Those imports have to name packages your project actually depends on, or they won't resolve. So the generator now reads the `package.json` next to the config it's generating for. A project that depends on `@prisma/orm-postgres` gets imports from that package. A project on today's names keeps today's names. Nothing to configure, because the manifest already says which it is. Contract hashes are unaffected, and that isn't an assumption — hashes are computed from a structure that import text never enters, and there's a test asserting the hash is identical across naming schemes *while* the emitted imports demonstrably differ. ## What stops the trap coming back Two checks, because the failure is silent and won't show up in a test suite: - Every example app and test project must use one naming scheme, not a mix. `lint-single-import-root` scans them and fails the build if any project imports from both, since that's the situation that loads code twice. - `lint-consumer-internal-imports` counts how many internal-package imports remain in those projects and compares against a committed number. It fails if the number goes up (someone added one) and also if it goes down without the number being updated (so improvements get locked in). Target is zero. The build itself also refuses to proceed if the published-package map would put one module in two places, or if a published package's `package.json` no longer matches what its code actually needs. ## Reading this PR It's large — 257 files — because it's a migration. The commits are grouped and meant to be read in order: 1. **Platform packages** — the build mechanism, and the seven platform packages it produces. 2. **Database packages, extension packs, the `prisma` command** — completes the set of 17. 3. **Generated imports become configurable** — one place decides which names get written, with today's names still the default. 4. **Database-family symmetry, publishing the map, the identity checks.** 5. **One package per application** — the three converted examples, the re-exports they proved necessary, and the counting check. 6. **Migration files follow the project too.** One thing worth knowing while reading: re-exporting a package republishes all of its sub-paths, not just the one that was needed. This PR adds 115 published sub-paths across the three database packages. Two candidates were dropped for exactly that reason — see below. ## Alternatives considered **Let an application install platform packages alongside its facade.** Nothing would need re-exporting and the facades would stay thinner. Rejected: an application would again juggle several Prisma dependencies whose correct combination it maintains by hand, and getting it wrong — upgrading one and not the other — produces the silent two-copies failure above. Re-exporting costs a generated line and nothing at runtime. **Re-export everything an application might plausibly want.** Rejected in review: because re-exporting brings a package's entire sub-path surface, generosity is expensive and hard to undo. Migration tooling (54 sub-paths) was dropped because its only users are extension packs, which install platform packages anyway; the SQL driver re-export was dropped because nothing imported it at all. What remains is what a converted example actually needed. **Flip the publish list in this same PR.** Rejected: it would mix "does the new surface work" with "is it safe to stop publishing 50 packages" in one review. The switch is mechanical once this lands, and gets its own change. ## Verification `build`, `typecheck` (156 tasks), `test:packages` (1077 files / 14087 tests), `test:e2e`, `lint`, `lint:deps`, `lint:docs`, `lint:manifests`, `check:publish-deps`, `check:clean-tree`, `lint:casts` and `lint:throws` (no new instances), `test:scripts`, coverage, the tarball-install suites, and regenerating every committed artifact leaves the tree unchanged. Known-unstable and unrelated to this change: the `relation-mode-gh-*` port suites (TML-3140), and several test timeouts that are too tight under load. ## Follow-ups TML-3124 switch the publish list · TML-3127 build cache can validate a stale published package on CI · TML-3140 unstable port suites · TML-3141 a test-helper sub-path reaches a package that is never published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added consolidated public ORM packages for PostgreSQL, MongoDB, SQLite, framework tooling, database targets, and extensions. - Generated contracts, migrations, and scaffolds now adapt imports to the consuming project’s package surface. - Added facade-provided `prisma-next` CLI access and consolidated migration entrypoints. - **Documentation** - Updated installation, package naming, public entrypoint, and migration scaffolding guidance. - **Tests** - Added coverage for package installation, exports, CLI behavior, module identity, and import compatibility. - **Chores** - Added checks preventing incompatible internal and public package imports. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat: an application depends on one Prisma package (ADR 242) (#29864) ## What changes for someone using Prisma Today, an application that talks to Postgres installs a long list of our packages: ```jsonc { "dependencies": { "@prisma-next/postgres": "...", "@prisma-next/sql-runtime": "...", "@prisma-next/sql-orm-client": "...", "@prisma-next/target-postgres": "...", "@prisma-next/adapter-postgres": "...", "@prisma-next/sql-contract": "..." // ...a dozen more } } ``` After this PR, it installs one: ```jsonc { "dependencies": { "@prisma/orm-postgres": "0.16.0" } } ``` Everything else arrives as that package's own dependencies. Three of our example apps are converted in this PR to prove it — one per database — and each has exactly one Prisma package in its `dependencies`. This implements [ADR 242](https://github.com/prisma/prisma/pull/29852), which is already merged. ## What gets published 17 packages, all under the `@prisma` scope: - **3 database packages** — `@prisma/orm-postgres`, `orm-sqlite`, `orm-mongo`. An application installs exactly one. We call these *facades*: each is a small package that wires its database together and re-exports everything an application needs. - **6 extension packs** — PostGIS, pgvector, ParadeDB, Supabase, arktype-json, middleware-cache. Optional, installed alongside a database package. - **7 platform packages** — the framework, the toolchain, one per database family, one per database target. Applications never install these directly; they arrive as dependencies. Extension authors do install them. - **the `prisma` command**, as a bin-only package. Every other workspace package — around 50 of them — stops being published. They still exist in the repo as the unit we organise code in; they just stop having a life on the registry. **This PR does not make that switch yet.** It builds and proves the new surface while leaving today's publish list exactly as it is. Flipping it is a separate change. ## The problem this design has to avoid A published package can't depend on packages that won't exist on the registry. So each published package *contains a compiled copy* of the internal packages it covers. That creates a trap. If one application ends up with the same code twice — once inside a published package, once as its own package — then classes, registries, and anything compared by reference exist twice too. An `instanceof` check quietly returns false. Nothing crashes, nothing fails to compile, and both copies behave identically in isolation. You find out much later, somewhere unrelated. So the rule the whole design follows is: **every piece of internal code is published from exactly one package.** Concretely, that means: - Each published package is built in one pass, so code shared between its own entry points exists once. Verified from the build's source maps: no module appears in more than one chunk, in any published package. - When one published package needs code from another, it imports it as a real dependency rather than compiling in a second copy. - A facade re-exports from the platform packages; it never carries its own copy. `@prisma/orm-postgres/orm-client` and `@prisma/orm-family-sql/orm-client` are two names for the same object, and there's a test that asserts exactly that from installed tarballs. - One table in `packages/0-shared/publish-surface` maps every internal package to where it's published. The build, the code generator, and the lint checks all read it, so there's one answer to "where does this live" rather than three that can drift. ## Generated code follows the application Prisma writes imports into your project — contract types and migration files. Those imports have to name packages your project actually depends on, or they won't resolve. So the generator now reads the `package.json` next to the config it's generating for. A project that depends on `@prisma/orm-postgres` gets imports from that package. A project on today's names keeps today's names. Nothing to configure, because the manifest already says which it is. Contract hashes are unaffected, and that isn't an assumption — hashes are computed from a structure that import text never enters, and there's a test asserting the hash is identical across naming schemes *while* the emitted imports demonstrably differ. ## What stops the trap coming back Two checks, because the failure is silent and won't show up in a test suite: - Every example app and test project must use one naming scheme, not a mix. `lint-single-import-root` scans them and fails the build if any project imports from both, since that's the situation that loads code twice. - `lint-consumer-internal-imports` counts how many internal-package imports remain in those projects and compares against a committed number. It fails if the number goes up (someone added one) and also if it goes down without the number being updated (so improvements get locked in). Target is zero. The build itself also refuses to proceed if the published-package map would put one module in two places, or if a published package's `package.json` no longer matches what its code actually needs. ## Reading this PR It's large — 257 files — because it's a migration. The commits are grouped and meant to be read in order: 1. **Platform packages** — the build mechanism, and the seven platform packages it produces. 2. **Database packages, extension packs, the `prisma` command** — completes the set of 17. 3. **Generated imports become configurable** — one place decides which names get written, with today's names still the default. 4. **Database-family symmetry, publishing the map, the identity checks.** 5. **One package per application** — the three converted examples, the re-exports they proved necessary, and the counting check. 6. **Migration files follow the project too.** One thing worth knowing while reading: re-exporting a package republishes all of its sub-paths, not just the one that was needed. This PR adds 115 published sub-paths across the three database packages. Two candidates were dropped for exactly that reason — see below. ## Alternatives considered **Let an application install platform packages alongside its facade.** Nothing would need re-exporting and the facades would stay thinner. Rejected: an application would again juggle several Prisma dependencies whose correct combination it maintains by hand, and getting it wrong — upgrading one and not the other — produces the silent two-copies failure above. Re-exporting costs a generated line and nothing at runtime. **Re-export everything an application might plausibly want.** Rejected in review: because re-exporting brings a package's entire sub-path surface, generosity is expensive and hard to undo. Migration tooling (54 sub-paths) was dropped because its only users are extension packs, which install platform packages anyway; the SQL driver re-export was dropped because nothing imported it at all. What remains is what a converted example actually needed. **Flip the publish list in this same PR.** Rejected: it would mix "does the new surface work" with "is it safe to stop publishing 50 packages" in one review. The switch is mechanical once this lands, and gets its own change. ## Verification `build`, `typecheck` (156 tasks), `test:packages` (1077 files / 14087 tests), `test:e2e`, `lint`, `lint:deps`, `lint:docs`, `lint:manifests`, `check:publish-deps`, `check:clean-tree`, `lint:casts` and `lint:throws` (no new instances), `test:scripts`, coverage, the tarball-install suites, and regenerating every committed artifact leaves the tree unchanged. Known-unstable and unrelated to this change: the `relation-mode-gh-*` port suites (TML-3140), and several test timeouts that are too tight under load. ## Follow-ups TML-3124 switch the publish list · TML-3127 build cache can validate a stale published package on CI · TML-3140 unstable port suites · TML-3141 a test-helper sub-path reaches a package that is never published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added consolidated public ORM packages for PostgreSQL, MongoDB, SQLite, framework tooling, database targets, and extensions. - Generated contracts, migrations, and scaffolds now adapt imports to the consuming project’s package surface. - Added facade-provided `prisma-next` CLI access and consolidated migration entrypoints. - **Documentation** - Updated installation, package naming, public entrypoint, and migration scaffolding guidance. - **Tests** - Added coverage for package installation, exports, CLI behavior, module identity, and import compatibility. - **Chores** - Added checks preventing incompatible internal and public package imports. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat: an application depends on one Prisma package (ADR 242) (#29864) ## What changes for someone using Prisma Today, an application that talks to Postgres installs a long list of our packages: ```jsonc { "dependencies": { "@prisma-next/postgres": "...", "@prisma-next/sql-runtime": "...", "@prisma-next/sql-orm-client": "...", "@prisma-next/target-postgres": "...", "@prisma-next/adapter-postgres": "...", "@prisma-next/sql-contract": "..." // ...a dozen more } } ``` After this PR, it installs one: ```jsonc { "dependencies": { "@prisma/orm-postgres": "0.16.0" } } ``` Everything else arrives as that package's own dependencies. Three of our example apps are converted in this PR to prove it — one per database — and each has exactly one Prisma package in its `dependencies`. This implements [ADR 242](https://github.com/prisma/prisma/pull/29852), which is already merged. ## What gets published 17 packages, all under the `@prisma` scope: - **3 database packages** — `@prisma/orm-postgres`, `orm-sqlite`, `orm-mongo`. An application installs exactly one. We call these *facades*: each is a small package that wires its database together and re-exports everything an application needs. - **6 extension packs** — PostGIS, pgvector, ParadeDB, Supabase, arktype-json, middleware-cache. Optional, installed alongside a database package. - **7 platform packages** — the framework, the toolchain, one per database family, one per database target. Applications never install these directly; they arrive as dependencies. Extension authors do install them. - **the `prisma` command**, as a bin-only package. Every other workspace package — around 50 of them — stops being published. They still exist in the repo as the unit we organise code in; they just stop having a life on the registry. **This PR does not make that switch yet.** It builds and proves the new surface while leaving today's publish list exactly as it is. Flipping it is a separate change. ## The problem this design has to avoid A published package can't depend on packages that won't exist on the registry. So each published package *contains a compiled copy* of the internal packages it covers. That creates a trap. If one application ends up with the same code twice — once inside a published package, once as its own package — then classes, registries, and anything compared by reference exist twice too. An `instanceof` check quietly returns false. Nothing crashes, nothing fails to compile, and both copies behave identically in isolation. You find out much later, somewhere unrelated. So the rule the whole design follows is: **every piece of internal code is published from exactly one package.** Concretely, that means: - Each published package is built in one pass, so code shared between its own entry points exists once. Verified from the build's source maps: no module appears in more than one chunk, in any published package. - When one published package needs code from another, it imports it as a real dependency rather than compiling in a second copy. - A facade re-exports from the platform packages; it never carries its own copy. `@prisma/orm-postgres/orm-client` and `@prisma/orm-family-sql/orm-client` are two names for the same object, and there's a test that asserts exactly that from installed tarballs. - One table in `packages/0-shared/publish-surface` maps every internal package to where it's published. The build, the code generator, and the lint checks all read it, so there's one answer to "where does this live" rather than three that can drift. ## Generated code follows the application Prisma writes imports into your project — contract types and migration files. Those imports have to name packages your project actually depends on, or they won't resolve. So the generator now reads the `package.json` next to the config it's generating for. A project that depends on `@prisma/orm-postgres` gets imports from that package. A project on today's names keeps today's names. Nothing to configure, because the manifest already says which it is. Contract hashes are unaffected, and that isn't an assumption — hashes are computed from a structure that import text never enters, and there's a test asserting the hash is identical across naming schemes *while* the emitted imports demonstrably differ. ## What stops the trap coming back Two checks, because the failure is silent and won't show up in a test suite: - Every example app and test project must use one naming scheme, not a mix. `lint-single-import-root` scans them and fails the build if any project imports from both, since that's the situation that loads code twice. - `lint-consumer-internal-imports` counts how many internal-package imports remain in those projects and compares against a committed number. It fails if the number goes up (someone added one) and also if it goes down without the number being updated (so improvements get locked in). Target is zero. The build itself also refuses to proceed if the published-package map would put one module in two places, or if a published package's `package.json` no longer matches what its code actually needs. ## Reading this PR It's large — 257 files — because it's a migration. The commits are grouped and meant to be read in order: 1. **Platform packages** — the build mechanism, and the seven platform packages it produces. 2. **Database packages, extension packs, the `prisma` command** — completes the set of 17. 3. **Generated imports become configurable** — one place decides which names get written, with today's names still the default. 4. **Database-family symmetry, publishing the map, the identity checks.** 5. **One package per application** — the three converted examples, the re-exports they proved necessary, and the counting check. 6. **Migration files follow the project too.** One thing worth knowing while reading: re-exporting a package republishes all of its sub-paths, not just the one that was needed. This PR adds 115 published sub-paths across the three database packages. Two candidates were dropped for exactly that reason — see below. ## Alternatives considered **Let an application install platform packages alongside its facade.** Nothing would need re-exporting and the facades would stay thinner. Rejected: an application would again juggle several Prisma dependencies whose correct combination it maintains by hand, and getting it wrong — upgrading one and not the other — produces the silent two-copies failure above. Re-exporting costs a generated line and nothing at runtime. **Re-export everything an application might plausibly want.** Rejected in review: because re-exporting brings a package's entire sub-path surface, generosity is expensive and hard to undo. Migration tooling (54 sub-paths) was dropped because its only users are extension packs, which install platform packages anyway; the SQL driver re-export was dropped because nothing imported it at all. What remains is what a converted example actually needed. **Flip the publish list in this same PR.** Rejected: it would mix "does the new surface work" with "is it safe to stop publishing 50 packages" in one review. The switch is mechanical once this lands, and gets its own change. ## Verification `build`, `typecheck` (156 tasks), `test:packages` (1077 files / 14087 tests), `test:e2e`, `lint`, `lint:deps`, `lint:docs`, `lint:manifests`, `check:publish-deps`, `check:clean-tree`, `lint:casts` and `lint:throws` (no new instances), `test:scripts`, coverage, the tarball-install suites, and regenerating every committed artifact leaves the tree unchanged. Known-unstable and unrelated to this change: the `relation-mode-gh-*` port suites (TML-3140), and several test timeouts that are too tight under load. ## Follow-ups TML-3124 switch the publish list · TML-3127 build cache can validate a stale published package on CI · TML-3140 unstable port suites · TML-3141 a test-helper sub-path reaches a package that is never published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added consolidated public ORM packages for PostgreSQL, MongoDB, SQLite, framework tooling, database targets, and extensions. - Generated contracts, migrations, and scaffolds now adapt imports to the consuming project’s package surface. - Added facade-provided `prisma-next` CLI access and consolidated migration entrypoints. - **Documentation** - Updated installation, package naming, public entrypoint, and migration scaffolding guidance. - **Tests** - Added coverage for package installation, exports, CLI behavior, module identity, and import compatibility. - **Chores** - Added checks preventing incompatible internal and public package imports. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat: an application depends on one Prisma package (ADR 242) (#29864) ## What changes for someone using Prisma Today, an application that talks to Postgres installs a long list of our packages: ```jsonc { "dependencies": { "@prisma-next/postgres": "...", "@prisma-next/sql-runtime": "...", "@prisma-next/sql-orm-client": "...", "@prisma-next/target-postgres": "...", "@prisma-next/adapter-postgres": "...", "@prisma-next/sql-contract": "..." // ...a dozen more } } ``` After this PR, it installs one: ```jsonc { "dependencies": { "@prisma/orm-postgres": "0.16.0" } } ``` Everything else arrives as that package's own dependencies. Three of our example apps are converted in this PR to prove it — one per database — and each has exactly one Prisma package in its `dependencies`. This implements [ADR 242](https://github.com/prisma/prisma/pull/29852), which is already merged. ## What gets published 17 packages, all under the `@prisma` scope: - **3 database packages** — `@prisma/orm-postgres`, `orm-sqlite`, `orm-mongo`. An application installs exactly one. We call these *facades*: each is a small package that wires its database together and re-exports everything an application needs. - **6 extension packs** — PostGIS, pgvector, ParadeDB, Supabase, arktype-json, middleware-cache. Optional, installed alongside a database package. - **7 platform packages** — the framework, the toolchain, one per database family, one per database target. Applications never install these directly; they arrive as dependencies. Extension authors do install them. - **the `prisma` command**, as a bin-only package. Every other workspace package — around 50 of them — stops being published. They still exist in the repo as the unit we organise code in; they just stop having a life on the registry. **This PR does not make that switch yet.** It builds and proves the new surface while leaving today's publish list exactly as it is. Flipping it is a separate change. ## The problem this design has to avoid A published package can't depend on packages that won't exist on the registry. So each published package *contains a compiled copy* of the internal packages it covers. That creates a trap. If one application ends up with the same code twice — once inside a published package, once as its own package — then classes, registries, and anything compared by reference exist twice too. An `instanceof` check quietly returns false. Nothing crashes, nothing fails to compile, and both copies behave identically in isolation. You find out much later, somewhere unrelated. So the rule the whole design follows is: **every piece of internal code is published from exactly one package.** Concretely, that means: - Each published package is built in one pass, so code shared between its own entry points exists once. Verified from the build's source maps: no module appears in more than one chunk, in any published package. - When one published package needs code from another, it imports it as a real dependency rather than compiling in a second copy. - A facade re-exports from the platform packages; it never carries its own copy. `@prisma/orm-postgres/orm-client` and `@prisma/orm-family-sql/orm-client` are two names for the same object, and there's a test that asserts exactly that from installed tarballs. - One table in `packages/0-shared/publish-surface` maps every internal package to where it's published. The build, the code generator, and the lint checks all read it, so there's one answer to "where does this live" rather than three that can drift. ## Generated code follows the application Prisma writes imports into your project — contract types and migration files. Those imports have to name packages your project actually depends on, or they won't resolve. So the generator now reads the `package.json` next to the config it's generating for. A project that depends on `@prisma/orm-postgres` gets imports from that package. A project on today's names keeps today's names. Nothing to configure, because the manifest already says which it is. Contract hashes are unaffected, and that isn't an assumption — hashes are computed from a structure that import text never enters, and there's a test asserting the hash is identical across naming schemes *while* the emitted imports demonstrably differ. ## What stops the trap coming back Two checks, because the failure is silent and won't show up in a test suite: - Every example app and test project must use one naming scheme, not a mix. `lint-single-import-root` scans them and fails the build if any project imports from both, since that's the situation that loads code twice. - `lint-consumer-internal-imports` counts how many internal-package imports remain in those projects and compares against a committed number. It fails if the number goes up (someone added one) and also if it goes down without the number being updated (so improvements get locked in). Target is zero. The build itself also refuses to proceed if the published-package map would put one module in two places, or if a published package's `package.json` no longer matches what its code actually needs. ## Reading this PR It's large — 257 files — because it's a migration. The commits are grouped and meant to be read in order: 1. **Platform packages** — the build mechanism, and the seven platform packages it produces. 2. **Database packages, extension packs, the `prisma` command** — completes the set of 17. 3. **Generated imports become configurable** — one place decides which names get written, with today's names still the default. 4. **Database-family symmetry, publishing the map, the identity checks.** 5. **One package per application** — the three converted examples, the re-exports they proved necessary, and the counting check. 6. **Migration files follow the project too.** One thing worth knowing while reading: re-exporting a package republishes all of its sub-paths, not just the one that was needed. This PR adds 115 published sub-paths across the three database packages. Two candidates were dropped for exactly that reason — see below. ## Alternatives considered **Let an application install platform packages alongside its facade.** Nothing would need re-exporting and the facades would stay thinner. Rejected: an application would again juggle several Prisma dependencies whose correct combination it maintains by hand, and getting it wrong — upgrading one and not the other — produces the silent two-copies failure above. Re-exporting costs a generated line and nothing at runtime. **Re-export everything an application might plausibly want.** Rejected in review: because re-exporting brings a package's entire sub-path surface, generosity is expensive and hard to undo. Migration tooling (54 sub-paths) was dropped because its only users are extension packs, which install platform packages anyway; the SQL driver re-export was dropped because nothing imported it at all. What remains is what a converted example actually needed. **Flip the publish list in this same PR.** Rejected: it would mix "does the new surface work" with "is it safe to stop publishing 50 packages" in one review. The switch is mechanical once this lands, and gets its own change. ## Verification `build`, `typecheck` (156 tasks), `test:packages` (1077 files / 14087 tests), `test:e2e`, `lint`, `lint:deps`, `lint:docs`, `lint:manifests`, `check:publish-deps`, `check:clean-tree`, `lint:casts` and `lint:throws` (no new instances), `test:scripts`, coverage, the tarball-install suites, and regenerating every committed artifact leaves the tree unchanged. Known-unstable and unrelated to this change: the `relation-mode-gh-*` port suites (TML-3140), and several test timeouts that are too tight under load. ## Follow-ups TML-3124 switch the publish list · TML-3127 build cache can validate a stale published package on CI · TML-3140 unstable port suites · TML-3141 a test-helper sub-path reaches a package that is never published. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Added consolidated public ORM packages for PostgreSQL, MongoDB, SQLite, framework tooling, database targets, and extensions. - Generated contracts, migrations, and scaffolds now adapt imports to the consuming project’s package surface. - Added facade-provided `prisma-next` CLI access and consolidated migration entrypoints. - **Documentation** - Updated installation, package naming, public entrypoint, and migration scaffolding guidance. - **Tests** - Added coverage for package installation, exports, CLI behavior, module identity, and import compatibility. - **Chores** - Added checks preventing incompatible internal and public package imports. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: willbot <w.a.madden+machine@gmail.com> Signed-off-by: Will Madden <madden@prisma.io> Co-authored-by: Claude Fable 5 <noreply@anthropic.com> | 1 个月前 | |
feat: target-declared aggregate codecs; count() returns bigint (TML-3064) (#29867) # Linked issue Refs [TML-3064](https://linear.app/prisma-company/issue/TML-3064/aggregate-codec-typing-and-extension-testkits) — the fifth and final slice of the [Codec JSON projections](https://linear.app/prisma-company/project/codec-json-projections-a10fba2e9cd5) project. Predecessors: #1023 (AST foundations), #1051 (codec descriptor protocols), #29844 (the lossless JSON hard cut). ## At a glance ```ts // test/integration/test/sql-builder/group-by.test.ts — the roadmap's witness, flipped: expect(alice!.cnt).toBe(2n); // was: expect(alice!.cnt).toBe('2') // test/integration/test/sql-orm-client/include-codec-canonical-json.test.ts: expect(total).toBe('9007199254740995'); // sum(int8) — the exact decimal, past 2^53 expect(peak).toBe(9007199254740993n); // max(int8) through an include's JSON envelope ``` Before this PR, aggregate values carried no codec: `count()` was typed `bigint` but returned whatever text the driver sent, and results were `Number()`-coerced — silently lossy past 2^53. ## Summary Aggregates were the last codec-blind read path after the lossless JSON hard cut. This PR gives them the same treatment columns received: targets declare what each aggregate returns, and everything downstream — emitted types, ORM planning, decoding, and the sql-builder lane — resolves through that one declaration. ## Decision 1. **`SqlAggregateDescriptor`**: a declarative mapping from `(operation, optional input codec)` to output codec + nullability + optional lowering, contributed beside `codecDescriptors`, validated at composition (never at query time), with exact-over-trait-over-input-agnostic precedence implemented once (`settleAggregateOverloads` in framework-components) and consumed by both the runtime registry and the emitter. 2. **Database-probed matrices** for PostgreSQL and SQLite, pinned two-sidedly by live conformance suites (a declared-but-wrong row fails one way; an unclaimed-but-supported pair fails the other). The targets genuinely diverge — PostgreSQL's integer `avg` is a `numeric` decimal string, SQLite's is a `real` number — so nothing is derived from a shared rule. 3. **`TypeMaps.aggregateTypes`**, emitted from the contributed descriptor set (deliberately not the target registry: the SQLite adapter contributes 9 of the target's 11 codecs, and emitting from the registry would advertise availability the runtime won't honour). 4. **The consumer cuts**: ORM planning stamps resolved output codecs (include aggregates become codec-projected JSON — no `native` aggregate entries remain), the `Number()`-coercion shims are deleted, decoding flows through the generic codec path, and the sql-builder lane's hardcoded `'pg/int8@1'` is retired with its static types now resolved from the emitted map. SQLite's bigint-output aggregates lower to `CAST(… AS TEXT)` so the driver can read them at all past 2^53. 5. **Public conformance testkits**: `@prisma-next/postgres-codec-testkit` and `@prisma-next/sqlite-codec-testkit` — dev-only, test-framework-independent packages extracted from the adapters' test-internal harnesses; pgvector and arktype-json consume them as packages instead of reaching across package boundaries with relative imports. 6. **The breaking-change record**: upgrade instructions for both audiences, a docs sweep for stale `number`-aggregate claims, and the codec authoring guide's new aggregate-descriptor section. ## Reviewer notes - **The three commits to spot-check**: `29d602143c` (the ORM cut — the largest), `8246165f97` / `51bbb2abb2` (the probed matrices — the doc blocks carry the probe findings and the unsupported lists). - **Two local gate reds at close, both ruled extrinsic** (full evidence in the review artifact's round notes): `test/cli-journeys` fails on `ERR_PNPM_IGNORED_BUILDS: esbuild@0.28.1` in the harness's temp-project installs — the esbuild version is identical on both sides of this diff (it came from #29812 on main) and this branch touches no build-approval config; and the adapter-postgres coverage run aborts on instrumentation-induced timeouts before computing numbers (the same suite passes 794/798 uninstrumented) — CI's Coverage job is the arbiter, and if its thresholds miss, the precedented remedy is a `warningOnly` entry in `coverage.config.json` for the tests-moved-to-break-a-cycle situation (the built-in conformance suites moved into the testkits because Turborepo's workspace graph cannot represent the adapter↔testkit dev-cycle). - **`sum`/`avg` outputs deliberately drop input type parameters** — a `numeric(10,3)` column sums to an unconstrained `numeric`; carrying the parameters into the result would overstate precision. - **HAVING operands stay `number`** on the result-typed surfaces — they are compared inside SQL against the aggregate the database computes and never cross a codec. The docs carve this out explicitly; a deliberate API-consistency decision is filed as a follow-up rather than flipped hastily. - **Project artifacts** under `projects/codec-json-projections/` (spec, plan, dispatch briefs, trace) ride this PR per the project convention; close-out removes the directory after the final retro. - **One test helper carries a typed workaround** for a real type-level inference gap (`IsToManyRelation` not recognising an in-file `.relations()` hasMany) — commented at both sites, follow-up drafted. ## How it fits together 1. **The protocol** (`6268028e13`, `24f1c819d1`): descriptor vocabulary split on the framework/SQL seam — the declarative half family-neutral (the emitter, which layering bars from the lanes, must read it), the lowering half in relational-core; contribution key beside `codecDescriptors`; composition-time validation with three documented error codes. 2. **The matrices** (`8246165f97`, `51bbb2abb2`): every built-in aggregate/input pair probed against a live database, declared or explicitly recorded unsupported. The probe earned its keep — `avg(float4)` widens to `float8` while `sum(float4)` does not; SQLite computes *something* for twelve pairs whose result class depends on the data, so they stay deliberately untyped. 3. **The input-agnostic match kind** (`6afc62e32f`): execution surfaced that `count(x)` matched nothing under the original three kinds; the spec was amended visibly and the fourth kind added — `count` declares one descriptor covering both forms. 4. **The emission** (`c137f6227e`, `4a26af3ea3`): `aggregateTypes` joins `TypeMaps`, pre-settled per contributed codec so type-level resolvers reproduce runtime precedence without re-deriving traits; 33 `contract.d.ts` fixtures regenerated, zero `contract.json` movement. 5. **The cuts** (`29d602143c`…`df63ce88f4`, `01f8c2d635`): ORM and lane resolve through the registry; coercion deleted; the lossless claim proven past 2^53 on both targets at both read paths, including the SQLite `CAST` lowering that makes the top-level read possible at all. 6. **The record** (`7811990a9c`…`8633f366fc`): upgrade instructions enumerated from the matrices, the docs sweep, the authoring guide's aggregate section, and the HAVING carve-out. ## Behavior changes & evidence - **`count()` returns `bigint`** on both targets, top-level and include; empty sets return `0n`. Implementation: `packages/3-extensions/sql-orm-client/src/aggregate-codecs.ts`, `query-plan-aggregate.ts`. Evidence: `test/integration/test/sql-orm-client/aggregate.test.ts`, `test/integration/test/sql-builder/group-by.test.ts`. - **Integer sums widen losslessly** (`pg/int8@1` → `bigint`; `sum(int8)`/integer `avg` → `pg/numeric@1` decimal strings; SQLite integer sums → `bigint`). Implementation: `packages/3-targets/3-targets/postgres/src/core/aggregates.ts`, `.../sqlite/src/core/aggregates.ts`. Evidence: the two testkit aggregate-conformance suites; `include-codec-canonical-json.test.ts` (values past 2^53). - **Include aggregates are codec-projected JSON** — no `native` aggregate entries remain. Implementation: `query-plan-select.ts`. Evidence: `packages/3-extensions/sql-orm-client/test/json-projection-emission.test.ts` (`expect(natives).toEqual([])`). - **SQLite bigint aggregates render `CAST(… AS TEXT)`** — the driver previously threw `RangeError` on any wide top-level aggregate. Implementation: `.../sqlite/src/core/aggregates.ts` (the protocol's first lowering hooks). Evidence: `sqlite-include-canonical-json.test.ts`, the sqlite testkit conformance suite. - **The conformance harnesses are published dev-only packages** consumed by adapters and extensions alike. Implementation: `packages/3-targets/6-adapters/{postgres,sqlite}-codec-testkit/`. Evidence: `pnpm lint:deps` (no production dependency), the extensions' migrated suites. ## Testing performed Per-dispatch gates across nine reviewed dispatches (two implementers, one persistent reviewer; 9 findings filed, 9 resolved), plus a full deferred-gate reckoning at close: adapter-postgres 794/798 (3 expected-fail, 1 skipped), pgvector 160/160, postgres testkit 134/134 (both live aggregate matrices), integration suites by directory — slice surfaces 463/464 (one 100ms timeout, green serially), ports 476 + 52 expected-fail, authoring/cross-package 108/108, mongo 148/148 — and the workspace confirmation: build 70/70, typecheck 146/146, `lint:deps` clean, `fixtures:check` no-op, `check:upgrade-coverage` green. The two extrinsic reds are described under Reviewer notes. ## Skill update `skills/prisma-next-queries` (per-target aggregate type table, HAVING carve-out, checklist) and `skills/upgrade/0.16-to-0.17` (two new entries: the aggregate break for users; the testkit migration + descriptor authoring for extension authors). `pnpm lint:skills` green. ## Follow-ups Three drafted tickets await Linear re-authorization (drafts in `plan.md § Open items`): the SQLite plain-column wide-bigint `RangeError` (independent of aggregates), the `IsToManyRelation` inference gap, and the HAVING typing asymmetry. The cli-journeys esbuild approval breakage is main's, from #29812. ## Alternatives considered - **Per-trait `count` descriptors or lane special-casing** instead of the input-agnostic kind — rejected: the first models count's result as input-dependent (false), the second puts operation knowledge in a generic planner. - **Emitting `aggregateTypes` from the target registry** — rejected: SQLite's adapter contributes a filtered codec set, and registry-derived emission would advertise availability the runtime won't honour. - **Driver-level `setReadBigInts`** instead of descriptor lowering — rejected: it changes the JS type of every integer column read; the lowering is scoped to the aggregates that need it. - **Trait fallbacks for `sum`/`avg`** — rejected: the probed populations disagree row by row; only `min`/`max` have probed-uniform trait populations. - **Keeping the conformance suites in the adapters** with a testkit devDependency — rejected by Turborepo itself: the workspace graph cannot represent the dev-cycle; the suites moved into the testkits, which also dogfoods the public API extension authors consume. ## Checklist - [x] All commits are signed off (`git commit -s`) per the DCO. - [x] I read CONTRIBUTING.md and the change is scoped to one logical concern. - [x] Tests are updated (extensively; nine dispatches of tests-first work). - [x] The PR title is in `TML-NNNN: <sentence-case title>` form. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Aggregate results now use database-accurate types across PostgreSQL and SQLite. * Counts and widened integer sums return `bigint`; averages may return decimal strings. * Aggregate nullability and supported operations are reflected in generated types. * Added target-aware handling for `count`, `sum`, `avg`, `min`, and `max`, including large values beyond JavaScript’s safe integer range. * Added aggregate codec conformance tooling and target-specific SQL result handling. * **Bug Fixes** * Empty counts return `0n`, while other empty aggregates remain `null`. * Unsupported or ambiguous combinations now produce structured errors. * SQLite preserves oversized integer aggregate values without numeric narrowing. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net> | 1 个月前 | |
TML-3163: add BigIntNumber and UnboundedInt column types (#29902) ## Linked issue Refs [TML-3163](https://linear.app/prisma-company/issue/TML-3163/opt-in-number-representation-integer-codecs-bigintnumber-unboundedint) — slice 06 of the [Codec JSON projections](https://linear.app/prisma-company/project/codec-json-projections-a10fba2e9cd5) project. This unblocks TML-3165, whose aggregate defaults consume the new codec IDs. This PR makes integer representation a per-column contract choice without changing the lossless `BigInt` default. ```prisma model Meter { id Int @id peak BigIntNumber lifetime UnboundedInt } ``` `peak` reads and writes as a JavaScript `number`, throwing outside ±(2^53 − 1) instead of rounding. `lifetime` uses PostgreSQL unconstrained `numeric` storage and round-trips integral values as exact JavaScript `bigint` values at arbitrary magnitude. ## Changes - **Integer representation codecs**: Adds `pg/int8number@1` and `sqlite/bigintnumber@1` for safe-range JavaScript numbers, plus PostgreSQL `pg/unboundedint@1` for arbitrary-precision integral values. Encode and decode paths reject non-integral or out-of-range values with structured `RUNTIME.ENCODE_FAILED` / `RUNTIME.DECODE_FAILED` errors. - **Target-scoped authored types**: PostgreSQL contributes `BigIntNumber` and `UnboundedInt`; SQLite contributes only `BigIntNumber`. These are top-level zero-argument type constructors, so PSL fields use ordinary bare type syntax and retain normal optional/default/list composition. The corresponding codecs keep `targetTypes: []`, leaving canonical introspection unchanged (`int8 → BigInt`, `numeric → Numeric`). - **TypeScript authoring**: The composed callback exposes `type.BigIntNumber()` and PostgreSQL `type.UnboundedInt()` for registered storage types used through `field.namedType(...)`. Direct authoring remains available through `field.column(pgInt8NumberColumn())`, `field.column(pgUnboundedIntColumn())`, and `field.column(sqliteBigintNumberColumn())`. - **Aggregate typing**: Adds target-probed `sum` / `avg` rows for the new codecs. `min` / `max` continue to resolve through the numeric-trait self fallback. PostgreSQL `sum` over `UnboundedInt` remains exact as `bigint`; widening results use the target's canonical numeric codec. - **End-to-end proof and migration guidance**: Adds PostgreSQL and SQLite emitted PSL fixtures, runtime and type-level ORM coverage, codec and aggregate conformance cases, reference documentation, and no-op upgrade declarations on the current `8.0.0-rc.1 → 8.0.0-rc.2` edge because existing source requires no migration. ## Why The database storage type cannot identify the intended application representation: PostgreSQL `int8` may be read as lossless `bigint` or guarded `number`, while `numeric` may represent general decimal text or integral `bigint`. Giving the alternative codecs native-type claims would make reverse resolution and introspection ambiguous. Target-contributed type constructors separate the two concerns cleanly: authors explicitly select the application representation, while introspection continues to emit the canonical type for each storage type. This also uses Prisma Next's surviving type-constructor abstraction rather than field-template machinery that would incorrectly impose preset-specific field restrictions. `BigIntNumber` deliberately projects database-produced JSON as a JSON number. The safe-range guard is sound because ECMAScript numbers are IEEE 754 binary64, 2^53 is exactly representable, and monotone rounding cannot move an out-of-range integer into the accepted safe range. Values that could lose precision always throw. ## Review notes - Registering the numeric codecs radiates additive `aggregateTypes.byCodec` rows into generated contracts even when a schema does not use the authored types. Existing entries remain unchanged. - SQLite has no `UnboundedInt` because it has no lossless unbounded integer storage. - On a flat SQLite read, `node:sqlite` may reject an out-of-range INTEGER before the codec runs; include/database-JSON reads still surface the structured codec error. - The integer-representation fixture outputs remain semantically unchanged after moving from call syntax to bare types; canonical regeneration adds only the expected globally radiated aggregate rows to one previously stale fixture. ## Validation Post-rebase validation against current `origin/main`: - `pnpm build` - `pnpm --dir test/integration typecheck` - Fresh PR Type Check job - `pnpm lint:deps` — 1,921 modules / 2,934 dependencies, no violations - `pnpm lint:skills` - `pnpm lint:docs` — passes with existing README warnings - `pnpm fixtures:check` - `pnpm check:upgrade-coverage` - PostgreSQL and SQLite target, scalar-parity, codec-conformance, aggregate-conformance, and contract-TS suites - Package-local typechecks for the changed target, extension, adapter-testkit, and contract-TS packages - Focused integer-representation integration: all 6 tests pass with no type errors - Stale authoring-call and preset-guidance `rg` gates - `git diff --check` All PR-scoped gates pass, including the fresh CI Type Check. Two local `pnpm typecheck` attempts hit a Turbo output-ordering race while concurrent builds cleaned package `dist` self-imports (`pgvector/pack`, then `supabase/runtime`); CI's isolated Type Check completes successfully. ## Checklist - [x] Commits are signed off per the DCO. - [x] Tests cover target availability, TypeScript authoring, codec boundaries, emitted contracts, runtime reads/writes, includes, and aggregate result types. - [x] Upgrade declarations classify the generated aggregate-row additions as inert for existing source on the current release edge. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added integer representation presets for PostgreSQL and SQLite, supporting safe-range JavaScript numbers and arbitrary-size PostgreSQL integers. * Added validation for unsafe, fractional, and out-of-range values. * Extended `avg`, `min`, `max`, and `sum` aggregates with nullable results and large-value support. * Added nested-read support and improved inferred types for these representations. * **Documentation** * Expanded guidance on integer presets, aggregate behavior, JSON formats, and validation errors. * **Tests** * Added comprehensive unit, integration, and aggregate conformance coverage. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net> | 1 个月前 | |
TML-3164: targets and extensions contribute aggregate operations (#29922) ## Linked issue Refs [TML-3164](https://linear.app/prisma-company/issue/TML-3164/contributed-aggregate-operations-de-hardcode-the-sql-builder-and-orm) — slice 07 of the [Codec JSON projections](https://linear.app/prisma-company/project/codec-json-projections-a10fba2e9cd5) project. Parallel with slice 06 ([TML-3163](https://linear.app/prisma-company/issue/TML-3163/opt-in-number-representation-integer-codecs-bigintnumber-unboundedint), PR #29902) — the two share no implementation surfaces. Both unblock [TML-3165](https://linear.app/prisma-company/issue/TML-3165/native-number-aggregate-defaults-countcountbigint-sumsumbigint), which adds `countBigInt`/`sumBigInt`/`avgDecimal` without touching either client package. Follow-up filed: [TML-3182](https://linear.app/prisma-company/issue/TML-3182/reserved-name-validation-for-contributed-aggregate-operations-in-the). ## At a glance A target or extension contributes an aggregate operation the framework has never heard of, and it appears on the client under its own name, typed and decoded: ```ts // contributed by a test-only extension: an exact pg/int8@1 input match, // declared output pg/int8@1, lowering to bit_or(...) const stats = await db.orm.Reading.aggregate((agg) => ({ bits: agg.bitOr('mask'), // contributed — bigint rows: agg.tally(), // contributed, no input — bigint total: agg.sum('mask'), // built-in — decimal string })); // { bits: 9007199254740995n, rows: 3n, total: '9007199254740995' } ``` `bitOr` and `sum` fold the same rows to the same digits and come back differently typed, because the declared output codec is the only thing that decides. Neither `bitOr` nor `tally` exists on a stack the extension is not composed into. ## Decision The aggregate operation set becomes a target/extension contribution end to end. No literal operation name and no per-operation logic survives in the sql-builder lane or sql-orm-client. 1. **The operation namespace is open; the SQL alphabet stays closed.** `AggregateFn` (`relational-core/src/ast/types.ts`) is SQL's alphabet, not the operation namespace. Registry assembly enforces the bridge: an operation named outside the alphabet must carry a `lower` hook, or composition fails with `RUNTIME.AGGREGATE_LOWERING_MISSING` — at composition, before any query. 2. **Out-of-alphabet operations are projection-only.** They exist only in lowered form, and a lowered form (SQLite's `CAST(… AS TEXT)`, say) is unsound in a comparison. HAVING, ORDER BY, and comparison operands refuse them with `ORM.AGGREGATE_PROJECTION_ONLY`, statically (`HavingBuilder` is keyed by `AggregateOperationNames<TContract> & AggregateFn`) and at runtime. 3. **Every consumer surface derives.** The SQL DSL's aggregate functions, the ORM's `aggregate()`, `groupBy().aggregate()`, `having()`, and the collection's include reducers read their method set from the contract's emitted `aggregateTypes` at the type level and from the composed registry at runtime. Arity follows row presence: `withoutInput` ⇒ zero-arg, `byCodec`/`anyInput` ⇒ field-taking, both ⇒ both. 4. **Reducers install as generated own-properties**, not through a Proxy, so `orm(...)` can reject a contributed name that would shadow a collection member with `ORM.AGGREGATE_OPERATION_RESERVED`. ## Reviewer notes - **Two observable changes, both intended, both recorded in the slice spec.** (a) `count(field)` now renders `COUNT(<column>)` where it previously dropped the argument and rendered `COUNT(*)` — PostgreSQL declares `count` with `input: { kind: 'any' }`, so both arities are honest data. It flips a recorded Prisma-deviation port assertion from `it.fails` to `it`. (b) For a contract whose aggregate map is unknown — an in-code `defineContract`, or a pre-`aggregateTypes` contract — the derived surfaces resolve to a branded empty type rather than five literal methods, so calls that compiled with an `as never` argument now need the builder cast. Two integration tests in this diff show that shape, and both upgrade clusters carry declarations. - **Why generated own-properties instead of a Proxy.** A proxy synthesises members on access, so there is nothing for reserved-name validation to enumerate — the reserved list would have to be hand-maintained, which is the hardcoding this slice removes. It also breaks the prototype chain that subclassing relies on (examples subclass `Collection`) and forbids the private-field access the reducers need. The cost is per-instance installation: builder chaining clones per step, so a five-link chain does ~25 `defineProperty` calls — noise against plan compilation, but worth knowing. - **`Collection` is now a type alias + construct-signature const**, with `CollectionBase` as the runtime class. The interface must declare exactly one construct signature; intersecting the class's static face instead produces `TS2510` at every subclass site. - **The lowering rule is enforced on the runtime plane only** — a descriptor with a novel name and no hook emits fine and fails at execution-context assembly. Deliberate: the three sibling registry validations already sit on that plane, and emission builds no expressions. Moving this one check would make the split less coherent. - **Empty-input results are now derived, not name-checked**: `emptyAggregateResult(nullable, codec)` replaces `fn === 'count' ? 0n : null`. Equivalence was verified by hand against both descriptor matrices — every built-in `count` is non-nullable and both bigint codecs decode `'0'` to `0n`. - The first commits carry planning artefacts shared with slice 06 (specs, project plan, design notes); `projects/**/trace.jsonl` will conflict trivially with that branch at merge — resolve by line union. ## How it fits together 1. **Open the vocabulary** — the registry accepts any operation name and enforces the lowering rule ([aggregate-descriptor-registry.ts](packages/2-sql/4-lanes/relational-core/src/aggregate-descriptor-registry.ts), with `isAggregateFn`/`aggregateFnNames` beside the union in [ast/types.ts](packages/2-sql/4-lanes/relational-core/src/ast/types.ts)). 2. **The lane cut** — the method set derives from the contract map, one generic funnel dispatches, and alphabet membership is its only branch ([expression.ts](packages/2-sql/4-lanes/sql-builder/src/expression.ts), [runtime/functions.ts](packages/2-sql/4-lanes/sql-builder/src/runtime/functions.ts), [runtime/expression-impl.ts](packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts)). 3. **The ORM cut** — the same derivation across include reducers, top-level and grouped aggregates, and HAVING ([types.ts](packages/3-extensions/sql-orm-client/src/types.ts), [collection.ts](packages/3-extensions/sql-orm-client/src/collection.ts), [aggregate-operations.ts](packages/3-extensions/sql-orm-client/src/aggregate-operations.ts)). 4. **Proof and record** — a contributed operation through a real query, plus ADR 020 and the descriptor guide. ## Behavior changes & evidence - **A contributed operation reaches the database and decodes through its declared codec.** Evidence: [contributed-aggregates.test.ts](test/integration/test/sql-orm-client/contributed-aggregates.test.ts) — top-level and include-reducer paths, the empty-input answer derived from declared nullability, the rendered SQL asserted to contain `bit_or(`, and the discriminator: the same query against a stack without the extension has no such method. - **`count(field)` counts the field.** Implementation: [aggregate-builder.ts](packages/3-extensions/sql-orm-client/src/aggregate-builder.ts). Evidence: the `legacy-aggregations` port assertion flips green. - **Out-of-alphabet operations are refused in comparison positions.** Implementation: [expression-impl.ts](packages/2-sql/4-lanes/sql-builder/src/runtime/expression-impl.ts). Evidence: [contributed-aggregates.test.ts](packages/3-extensions/sql-orm-client/test/contributed-aggregates.test.ts) (HAVING refusal) and the lane's [runtime/contributed-aggregates.test.ts](packages/2-sql/4-lanes/sql-builder/test/runtime/contributed-aggregates.test.ts). - **Reserved names are rejected at composition.** Implementation: [orm.ts](packages/3-extensions/sql-orm-client/src/orm.ts). Evidence: a test walks a live collection's own property names and fails if the guarded list misses one — the set cannot silently drift. ## Testing performed - `pnpm build`, `pnpm typecheck:all` (packages + examples), `pnpm lint:deps` (1922 modules, 0 violations), `pnpm lint` on touched packages — green - `pnpm test` for sql-orm-client / sql-builder / relational-core — green (717 / 157 / 448) - Integration suite in four shards, all 318 files — green apart from two host-environment failures confirmed by signature and passing in isolation (`issues-28192-pg-historical-dates`, host timezone; `init-journey.e2e`, host pnpm) - `pnpm fixtures:check` — **zero movement**; `pnpm check:upgrade-coverage`, `pnpm check:error-reference` (260 codes), `pnpm lint:docs`, `pnpm lint:skills` — green - Slice gates: `rg "'(count|sum|avg|min|max)'"` over both client src trees and `rg "createIncludeScalar\('"` over all code — both empty. Cast ratchet `delta=-5`. ## Skill update Both upgrade clusters carry declarations for `8.0.0-rc.1-to-8.0.0-rc.2`: the extension-author cluster covers stub execution contexts needing an aggregate registry, the map-less-contract surface change, `count(field)`, and the contributed-operation rules; the app cluster covers the two changes reachable from the TS-authored no-emit path. The shipped query guide (`skills/prisma-8/references/queries-postgres.md`) gains the include-reducer documentation it never had. ## Follow-ups - [TML-3182](https://linear.app/prisma-company/issue/TML-3182/reserved-name-validation-for-contributed-aggregate-operations-in-the) — the lane's `fn` namespace has the same shadowing property as the collection surface and no reserved-name check; a contributed `bit_and → 'and'` would quietly shadow the built-in. ## Alternatives considered - **A Proxy for runtime dispatch** (the original working position) — rejected on three counts during implementation: nothing to enumerate for reserved-name validation, a broken prototype chain for subclassers, and no private-field access from a proxy receiver. - **Opening the AST `AggregateFn` union to `string`** — unnecessary. Lowering hooks and the existing function nodes cover contributed operations, and the closed union keeps renderers exhaustive. - **Supporting out-of-alphabet operations in HAVING** — a lowered form is unsound in a comparison, so honest support needs design work nothing currently requires. Projection-only with a structured refusal is the truthful shape. - **Moving the lowering check to emission** — would make the plane split less coherent, not more; the sibling registry validations all live on the runtime plane. - **Bare `unknown` as the map-less guard** — worked, but deleted the named diagnostic. A branded empty type preserves intersection identity and restores "the contract declares no aggregate operations" in the hover. ## Checklist - [x] All commits are signed off (`git commit -s`) per the [DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO status check will block merge if any commit is missing a `Signed-off-by:` trailer. - [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is scoped to one logical concern. - [x] Tests are updated (or `n/a` if the change is doc-only / refactor with no behavioural delta). - [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear ticket prefix + concise title naming the concrete deliverable). See `.claude/skills/create-pr/SKILL.md` for the full convention. - [x] The **Skill update** section above is filled in (or stated `n/a — internal only`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Added support for custom aggregate operations contributed through extensions. * Aggregate methods, result types, nullability, and valid call shapes are now derived dynamically. * Added field-aware and zero-input aggregates, including relation include reductions. * Added lowering support for non-standard aggregates and projection-only operation restrictions. * Empty aggregate results now respect declared codecs and nullability. * **Bug Fixes** * Added validation and structured errors for unsupported, reserved, or improperly lowered operations. * **Documentation** * Expanded aggregate guides, error references, architecture rules, and upgrade instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net> | 1 个月前 | |
TML-3165: count/sum/avg return JS numbers, with lossless variants beside them (#29930) ## Linked issue Refs [TML-3165](https://linear.app/prisma-company/issue/TML-3165/native-number-aggregate-defaults-countcountbigint-sumsumbigint) — slice 08, the last of the [Codec JSON projections](https://linear.app/prisma-company/project/codec-json-projections-a10fba2e9cd5) project. **Stacked on [#29922](https://github.com/prisma/prisma/pull/29922)** (TML-3164) and targets that branch until it merges, then advances to `main`. Prerequisites: slice 06 ([#29902](https://github.com/prisma/prisma/pull/29902), merged) supplied the codecs this names; slice 07 (#29922) made the operation set a contribution, so this PR adds three operations without touching a line of client or lane code. Follow-ups filed: [TML-3187](https://linear.app/prisma-company/issue/TML-3187/schema-written-literals-are-not-application-values-give-them-their-own). ## At a glance ```ts const stats = await db.orm.Order.aggregate((agg) => ({ orders: agg.count(), // number — was bigint cents: agg.sum('amountCents'), // number — was bigint; throws past 2^53 mean: agg.avg('amountCents'), // number — was a decimal string exactOrders: agg.countBigInt(), // bigint exactCents: agg.sumBigInt('amountCents'), // bigint, exact past 2^63 exactMean: agg.avgDecimal('amountCents'), // decimal string })); ``` Since the aggregate hard cut, `count()` returned a `bigint` — which `JSON.stringify` refuses — and integer `avg()` returned a decimal string. Correct, but not what a JS developer expects. This restores the expected types without restoring the silent corruption they used to hide: where a value cannot fit, the codec throws. ## Decision The aggregate vocabulary splits in two, by policy: 1. **Bare operations answer in JS-native types.** `count()` and `sum()` over integers return `number` through a guarded codec that raises `RUNTIME.DECODE_FAILED` past the safe-integer range rather than handing back a rounded value. `avg()` returns `number` through `float8` — a mean is a fraction already, so there is nothing to guard. 2. **Suffixed operations are lossless.** `countBigInt()`, `sumBigInt()`, `avgDecimal()`. Offered over every integer input, including those whose bare form is already lossless, so the escape hatch is uniform rather than something you learn column by column. 3. **Bare operations over Float and Decimal columns stay in the column's own family** — those users already chose their representation. `min`/`max` return the column's own type and are untouched. 4. **A non-nullable aggregate descriptor now declares `emptyResultJson`** — the empty-input answer in its result codec's canonical JSON. It belongs to the operation, not the codec: `count`'s identity is zero, but a `every()` would answer `true`. Classic Prisma is the prior art — `BigInt` columns are `bigint` there while `count` is a `number` — but where it casts down in the engine, this throws at the boundary. ## Reviewer notes - **Read the two matrices first** (`packages/3-targets/3-targets/{postgres,sqlite}/src/core/aggregates.ts`). They are the entire judgment; everything else derives. Every row was probed against a live database before it was authored. - **Three facts are load-bearing, and each has a test that fails if it is quietly substituted.** `sumBigInt` over `int8` reads PostgreSQL's `numeric` through `pg/unboundedint@1` rather than casting to `int8` — the cast is exercised *as a negative in the same test*, raising `bigint out of range` over the data the shipped row reads exactly. `avg` casts the **result**, not the input, pinned on a dataset where the two genuinely differ (`4503599627370497` vs `...496`). `emptyResultJson` cannot be omitted: the type is a discriminated union, so a `nullable: false` descriptor without it does not compile. - **Three substrate repairs the matrices exposed rather than caused**, each a stale assumption that held only while every non-nullable aggregate decoded through a bigint codec. SQLite's number-flavoured codec needed a JSON projection (its transport cast renders a JSON *string* inside an envelope, so every SQLite include aggregate was failing to decode — and no test covered that path, which is why CI stayed green over it). The integer codecs now distinguish a wrong JS type from a wrong magnitude — which uncovered that the bigint codecs had been *silently accepting* JS numbers, so `1.5` could reach an integer column as `'1.5'`. And the DDL renderers compose `encode(decodeJson(stored))` instead of feeding canonical JSON to `encode`, which also fixed a `timestamptz` default handed an ISO string where the codec declares a `Date`. - **One acknowledged stopgap.** Tightening those guards broke `BigInt @default(0)`: a schema language writes no `bigint`, so PSL literals arrive as JSON numbers, and emission of the Supabase extension's contract stopped. `encodeJson` now accepts a safe-integer `number` (guarded — integral, in-range) while the wire `encode` stays strict. The proper seam is TML-3187. Reviewed as safe: `encodeJson` is unreachable from the runtime parameter path. - **~100 regenerated `contract.d.ts` files.** All movement is inside `export type AggregateTypes` — verified mechanically: `git diff -U0` yields 247 hunks under that one header and no other. No `contract.json` and no migration fixture moved. - A local fresh-eyes review ran before this PR; its three MUST-FIX findings were all in the documentation, not the code, and are fixed here. ## How it fits together 1. **The PostgreSQL matrix** — the policy, probed and authored, with database-backed conformance evidence. 2. **The SQLite matrix** — the same policy in SQLite's terms; `avgDecimal` is not contributed (no decimal), and its absence is asserted as unavailability rather than a runtime error. 3. **The substrate repairs** — the three above, at their source. 4. **The sweep** — contracts regenerated, every moved expectation classified as *mechanical form change* or *corrected defect*; five tests re-expressed against `sumBigInt` because they asserted that a wide bare `sum` survives, which the policy now forbids. 5. **The record** — upgrade instructions in both clusters, a 13-pattern docs sweep, ADR 020 and the descriptor guide. ## Behavior changes & evidence - **`count()`/`sum()` return `number` and throw past 2^53** rather than rounding — on the wire path *and* the include/JSON path, where the value is emitted as a JSON number, rounded by `JSON.parse`, and refused by the post-parse guard. Evidence: [integer-representation.test.ts](test/integration/test/sql-orm-client/integer-representation.test.ts), both cases with whole error shapes. - **`sumBigInt()` is exact past 2^63** on PostgreSQL. Evidence: [aggregate-defaults.integration.test.ts](packages/3-targets/6-adapters/postgres-codec-testkit/test/aggregate-defaults.integration.test.ts) — `18446744073709551614n`, beside the `int8` cast raising. - **`avg()` returns a `number`, `avgDecimal()` a decimal string**, pinned on a non-terminating mean so the two visibly differ. - **SQLite include aggregates decode again**, as JSON numbers. Evidence: [sqlite-include-canonical-json.test.ts](test/integration/test/sql-orm-client/sqlite-include-canonical-json.test.ts) — the first committed coverage of that path. ## Testing performed - `pnpm build`, `pnpm typecheck:all` (92 tasks), `pnpm lint:deps` (no violations), `pnpm lint` — green - `pnpm test:packages` — 1113 files, 14,776 tests green; `pnpm test:e2e` — 113 green - Full unsharded `pnpm test:integration` — green apart from two host-environment files reproduced independently of this branch (`init-journey.e2e`, host pnpm; `issues-28192-pg-historical-dates`, host timezone) - `pnpm fixtures:check` green with movement fully attributable; `check:upgrade-coverage`, `check:error-reference` (274 codes), `lint:docs`, `lint:skills` green; cast ratchet `delta=-5` ## Skill update Both upgrade clusters carry entries for `8.0.0-rc.1-to-8.0.0-rc.2`: the app cluster covers the result-type flips and the integer columns now refusing a wrong JS type; the extension cluster adds the `emptyResultJson` obligation and the `encode`/`encodeJson` split. Entries slice 07 wrote in the same transition were corrected where this slice falsified them. The shipped query guide's aggregate result-type table is rewritten. ## Follow-ups - [TML-3187](https://linear.app/prisma-company/issue/TML-3187/schema-written-literals-are-not-application-values-give-them-their-own) — schema-written literals need their own codec seam, distinct from `encodeJson`'s application-value contract; includes the related gap that the TS authoring surface cannot express a `bigint` default at all. ## Alternatives considered - **Casting `sumBigInt` to `int8`** — simpler, and wrong: it reintroduces a 64-bit overflow this design does not have, and would resurrect the need for a `sumDecimal` the design discarded. - **Casting `avg`'s input rather than its result** — changes accumulation semantics; the result cast computes the exact mean once and rounds once. - **A codec-side "canonical zero" for the empty-input answer** — it can only serve operations whose identity is zero, and asks every codec in the stack a question most cannot answer. - **Skipping the transport lowering inside a JSON envelope** (the obvious fix for the SQLite defect) — wrong: the lossless variants' lowerings are semantic, not transport, so skipping them computes nothing. - **Withholding the lossless variant where the bare form is already lossless** — logically tidy, but it makes the escape hatch conditional on knowledge a caller shouldn't need. ## Checklist - [x] All commits are signed off (`git commit -s`) per the [DCO](../CONTRIBUTING.md#developer-certificate-of-origin-dco). The DCO status check will block merge if any commit is missing a `Signed-off-by:` trailer. - [x] I read [CONTRIBUTING.md](../CONTRIBUTING.md) and the change is scoped to one logical concern. - [x] Tests are updated (or `n/a` if the change is doc-only / refactor with no behavioural delta). - [x] The PR title is in `TML-NNNN: <sentence-case title>` form (Linear ticket prefix + concise title naming the concrete deliverable). See `.claude/skills/create-pr/SKILL.md` for the full convention. - [x] The **Skill update** section above is filled in (or stated `n/a — internal only`). <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Aggregate `count`, integer `sum`, and integer `avg` results now use JavaScript numbers by default. * Added lossless `countBigInt`, `sumBigInt`, and PostgreSQL `avgDecimal` options for exact results. * Aggregate results now follow the selected database target and field representation. * **Bug Fixes** * Unsafe numeric results beyond JavaScript’s safe-integer range now raise a runtime error. * Non-nullable aggregates correctly return their defined empty-result values. * **Documentation** * Updated aggregate behavior, codec guidance, error references, and upgrade instructions. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net> | 1 个月前 |
| 文件 | 最后提交记录 | 最后更新时间 |
|---|---|---|
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 | ||
| 1 个月前 |