From ad9c9ea36c90d9a32fd00872a8a9d1ba63a258d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20Berg?= Date: Tue, 5 May 2026 16:01:47 +0200 Subject: [PATCH 1/4] fix(sql-orm-client): selectIncludeStrategy reads capability flags from the contract's target/family namespaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `selectIncludeStrategy` previously checked `contract.capabilities['lateral']` and `contract.capabilities['jsonAgg']` at the top level only. The `Contract` type declares capabilities as `Record>` — a namespaced map — and the contract emitter populates the flags one level deeper, under the contract's `targetFamily` (e.g. `'sql'`) and `target` (e.g. `'postgres'`) namespaces. Top-level access never found them, so every nested include fell back to the multi-query strategy on emitted contracts even when the runtime supported single-query lateral / correlated emission. Concrete impact: a 2-level include like `db.User.include('comments').all()` fired 2 SQL statements instead of the expected 1. Why CI didn't catch it: the strategy unit tests and the integration tests that exercised the lateral path all populated capabilities at the top level (`{ lateral: { enabled: true }, jsonAgg: { enabled: true } }`), a shape the buggy detection happened to match but that no real emitted contract produces. The lateral / correlated branches of the dispatcher were dead code in practice, but tests appeared to cover them. The fix uses the contract's own declared identity: contract.capabilities[contract.targetFamily]?.[flag] === true || contract.capabilities[contract.target]?.[flag] === true Universal SQL flags like `jsonAgg` declared by the SQL family at `capabilities.sql.jsonAgg` are picked up; target-specific flags like `lateral` declared at `capabilities.postgres.lateral` are picked up; flags split across both namespaces compose correctly. Cross-namespace false positives ("`postgres.lateral` found while running SQLite") are impossible because we only inspect the running target's namespaces. Tests: - `include-strategy.test.ts` rewritten to use the principled shape: `{ sql: { jsonAgg: true } }`, `{ postgres: { jsonAgg: true, lateral: true } }`, split namespaces, unrelated namespaces, explicit false, non-boolean values. The previous suite tested the buggy top-level shape; those scenarios were never reachable from an emitted contract and have been replaced. - `collection-dispatch.test.ts` `withSingleQueryCapabilities` helper updated to populate capabilities under the contract's actual target/family namespaces rather than at the top level. Same logical intent, now matching the type's shape. - `integration/include.test.ts` `createUsersCollectionWithCapabilities` helper now replaces capabilities entirely (instead of merging with the base contract's `postgres` namespace), so passing `{ sql: { jsonAgg: true } }` truly tests the correlated path. Four call sites updated to use the namespaced shape. All 479 sql-orm-client tests pass. --- .../sql-orm-client/src/include-strategy.ts | 50 ++++++++--- .../test/collection-dispatch.test.ts | 16 +++- .../test/include-strategy.test.ts | 85 +++++++++++-------- .../test/integration/include.test.ts | 22 ++--- 4 files changed, 110 insertions(+), 63 deletions(-) diff --git a/packages/3-extensions/sql-orm-client/src/include-strategy.ts b/packages/3-extensions/sql-orm-client/src/include-strategy.ts index 1c2a424011..acd7fc2e23 100644 --- a/packages/3-extensions/sql-orm-client/src/include-strategy.ts +++ b/packages/3-extensions/sql-orm-client/src/include-strategy.ts @@ -3,10 +3,29 @@ import type { SqlStorage } from '@prisma-next/sql-contract/types'; export type IncludeStrategy = 'lateral' | 'correlated' | 'multiQuery'; +/** + * Choose the SQL emission strategy for nested includes based on the + * contract's declared capabilities. + * + * - `'lateral'`: outer SELECT with one LATERAL JOIN per relation, + * aggregating to JSON. Requires both `lateral` and `jsonAgg`. + * Postgres has both. + * - `'correlated'`: outer SELECT with one correlated subquery per + * relation, aggregating to JSON. Requires `jsonAgg` only. + * SQLite has `jsonAgg` (via `json_group_array`) but no LATERAL. + * - `'multiQuery'`: fallback. One SELECT per relation, stitched + * together in JS via `WHERE pk IN (parent-pk-values)`. Always + * correct; just N+1 round-trips. + * + * The capability flags are looked up under the contract's + * `targetFamily` and `target` namespaces — the two layers the contract + * emitter actually populates. Cross-namespace ("`postgres.lateral` + * found while running SQLite") false positives are impossible because + * we only inspect the running target's namespaces. + */ export function selectIncludeStrategy(contract: Contract): IncludeStrategy { - const capabilities = contract.capabilities as Record | undefined; - const hasLateral = hasCapability(capabilities?.['lateral']); - const hasJsonAgg = hasCapability(capabilities?.['jsonAgg']); + const hasLateral = capabilityFlag(contract, 'lateral'); + const hasJsonAgg = capabilityFlag(contract, 'jsonAgg'); if (hasLateral && hasJsonAgg) { return 'lateral'; @@ -19,15 +38,18 @@ export function selectIncludeStrategy(contract: Contract): IncludeSt return 'multiQuery'; } -function hasCapability(value: unknown): boolean { - if (value === true) { - return true; - } - - if (typeof value !== 'object' || value === null) { - return false; - } - - const flags = value as Record; - return Object.values(flags).some((flag) => flag === true); +/** + * Read a capability flag from the contract's target/family namespaces. + * + * The contract emitter populates `capabilities[targetFamily]` (universal + * SQL flags like `jsonAgg`, `returning`) and `capabilities[target]` + * (target-specific flags like `lateral` on Postgres). Either may + * declare a given flag; the family namespace declares the floor and the + * target namespace can extend on top. + */ +function capabilityFlag(contract: Contract, flag: string): boolean { + return ( + contract.capabilities[contract.targetFamily]?.[flag] === true || + contract.capabilities[contract.target]?.[flag] === true + ); } diff --git a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts index bdb06a1bed..4f91bd1f6d 100644 --- a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts +++ b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts @@ -13,8 +13,15 @@ function withSingleQueryCapabilities(contract: TestContract): TestContract { ...contract, capabilities: { ...contract.capabilities, - lateral: { enabled: true }, - jsonAgg: { enabled: true }, + [contract.targetFamily]: { + ...(contract.capabilities[contract.targetFamily] ?? {}), + jsonAgg: true, + }, + [contract.target]: { + ...(contract.capabilities[contract.target] ?? {}), + jsonAgg: true, + lateral: true, + }, }, } as unknown as TestContract; } @@ -195,7 +202,10 @@ describe('collection-dispatch', () => { }); it('dispatchCollectionRows() multi-query path stitches includes, strips hidden fields, and releases scope', async () => { - const contract = getTestContract(); + // Force multi-query strategy by clearing capabilities. Otherwise + // the base test contract's postgres.lateral / postgres.jsonAgg + // would route to single-query lateral. + const contract = { ...getTestContract(), capabilities: {} } as unknown as TestContract; const { collection, runtime } = createCollectionFor('User', contract); const scoped = collection.select('name').include('posts', (posts) => posts.select('title')); diff --git a/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts b/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts index fe43ebf275..7f4ef3c518 100644 --- a/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts +++ b/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts @@ -2,81 +2,94 @@ import { describe, expect, it } from 'vitest'; import { selectIncludeStrategy } from '../src/include-strategy'; import { getTestContract } from './helpers'; +// The default test contract has `target: 'postgres'`, `targetFamily: 'sql'`, +// and capabilities populated under those two namespaces. The strategy +// selector reads only those namespaces, so override `capabilities` +// directly to drive each scenario. + describe('selectIncludeStrategy', () => { it('returns multiQuery when include capabilities are absent', () => { - const contract = getTestContract(); - const strategy = selectIncludeStrategy(contract); + const contract = { + ...getTestContract(), + capabilities: {}, + } as unknown as ReturnType; - expect(strategy).toBe('multiQuery'); + expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); - it('returns correlated when jsonAgg is enabled without lateral', () => { + it('returns correlated when jsonAgg is enabled in the family namespace without lateral', () => { const contract = { ...getTestContract(), capabilities: { - jsonAgg: { - enabled: true, - }, + sql: { jsonAgg: true }, }, - }; + } as unknown as ReturnType; - const strategy = selectIncludeStrategy(contract); - expect(strategy).toBe('correlated'); + expect(selectIncludeStrategy(contract)).toBe('correlated'); }); - it('returns lateral when both lateral and jsonAgg are enabled', () => { + it('returns lateral when both flags are enabled in the same namespace', () => { const contract = { ...getTestContract(), capabilities: { - lateral: { - enabled: true, - }, - jsonAgg: { - enabled: true, - }, + postgres: { jsonAgg: true, lateral: true }, }, - }; + } as unknown as ReturnType; - const strategy = selectIncludeStrategy(contract); - expect(strategy).toBe('lateral'); + expect(selectIncludeStrategy(contract)).toBe('lateral'); }); - it('reads object capabilities via enabled flag', () => { + it('returns lateral when flags are split across family and target namespaces', () => { + // Real-world shape: SQL family declares `jsonAgg`; the postgres + // target adds `lateral` on top. const contract = { ...getTestContract(), capabilities: { - lateral: { enabled: true }, - jsonAgg: { enabled: false, fallback: true }, + sql: { jsonAgg: true }, + postgres: { lateral: true }, }, - }; + } as unknown as ReturnType; + + expect(selectIncludeStrategy(contract)).toBe('lateral'); + }); + + it('ignores capability flags in unrelated namespaces', () => { + // The default test contract's target/family are 'postgres' / 'sql'. + // A `mongo: { lateral: true }` namespace must not enable lateral + // on a postgres runtime — namespaces are scoped to the running + // target/family. + const contract = { + ...getTestContract(), + capabilities: { + mongo: { jsonAgg: true, lateral: true }, + nonsense: { lateral: true }, + }, + } as unknown as ReturnType; - const strategy = selectIncludeStrategy(contract); - expect(strategy).toBe('lateral'); + expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); - it('accepts top-level boolean capability flags', () => { + it('treats non-boolean capability values as missing', () => { + // The Contract type declares capability values as `boolean`. Anything + // else (string, object, undefined) is treated as not present. const contract = { ...getTestContract(), capabilities: { - lateral: true, - jsonAgg: true, + sql: { jsonAgg: 'yes' as unknown as boolean, lateral: true }, }, } as unknown as ReturnType; - const strategy = selectIncludeStrategy(contract); - expect(strategy).toBe('lateral'); + expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); - it('ignores non-boolean, non-object capability values', () => { + it('treats explicit `false` as not enabled', () => { const contract = { ...getTestContract(), capabilities: { - lateral: 'yes', - jsonAgg: true, + sql: { jsonAgg: true, lateral: false }, }, } as unknown as ReturnType; - const strategy = selectIncludeStrategy(contract); - expect(strategy).toBe('correlated'); + expect(selectIncludeStrategy(contract)).toBe('correlated'); }); }); diff --git a/packages/3-extensions/sql-orm-client/test/integration/include.test.ts b/packages/3-extensions/sql-orm-client/test/integration/include.test.ts index 9c09504542..5b2de377ee 100644 --- a/packages/3-extensions/sql-orm-client/test/integration/include.test.ts +++ b/packages/3-extensions/sql-orm-client/test/integration/include.test.ts @@ -47,12 +47,14 @@ function createUsersCollectionWithCapabilities( capabilities: Record, ) { const base = getTestContract(); + // Replace capabilities entirely (rather than merging with base) so the + // test's intent is unambiguous. Merging with the base contract's + // postgres namespace would leak `postgres.lateral` and `postgres.jsonAgg` + // into the strategy detection — making it impossible to test the + // correlated-only path by passing only `jsonAgg`. const contract = { ...base, - capabilities: { - ...base.capabilities, - ...capabilities, - }, + capabilities, } as typeof base; const context = { ...getTestContext(), contract }; @@ -333,8 +335,7 @@ describe('integration/include', () => { async () => { await withCollectionRuntime(async (runtime) => { const users = createUsersCollectionWithCapabilities(runtime, { - lateral: { enabled: true }, - jsonAgg: { enabled: true }, + postgres: { jsonAgg: true, lateral: true }, }); await seedUsers(runtime, [ @@ -412,8 +413,7 @@ describe('integration/include', () => { async () => { await withCollectionRuntime(async (runtime) => { const users = createUsersCollectionWithCapabilities(runtime, { - lateral: { enabled: true }, - jsonAgg: { enabled: true }, + postgres: { jsonAgg: true, lateral: true }, }); await seedUsers(runtime, [ @@ -506,7 +506,8 @@ describe('integration/include', () => { async () => { await withCollectionRuntime(async (runtime) => { const users = createUsersCollectionWithCapabilities(runtime, { - jsonAgg: { enabled: true }, + // jsonAgg without lateral → correlated subquery strategy. + sql: { jsonAgg: true }, }); await seedUsers(runtime, [{ id: 1, name: 'Alice', email: 'alice@example.com' }]); @@ -547,7 +548,8 @@ describe('integration/include', () => { async () => { await withCollectionRuntime(async (runtime) => { const users = createUsersCollectionWithCapabilities(runtime, { - jsonAgg: { enabled: true }, + // jsonAgg without lateral → correlated subquery strategy. + sql: { jsonAgg: true }, }); await seedUsers(runtime, [ From 32cefcfc23005c3eabe2ee32edcd216c38dccd83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20Berg?= Date: Tue, 5 May 2026 16:03:17 +0200 Subject: [PATCH 2/4] test(sql-orm-client): regression guards for namespaced capability lookup; centralize the structural cast in a `withCapabilities` helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds two tests that assert depth-1 includes against an emitted-shape contract (capabilities under `targetFamily` and `target` namespaces) fire exactly one SQL execution. Either fails fast if `selectIncludeStrategy`'s namespace-aware lookup regresses. Verified the unit-level guard fails when the strategy detection reverts to top-level-only access. Test ergonomics: - Adds `withCapabilities(contract, capabilities)` to `helpers.ts`. The narrow `TestContract` type fixes capabilities to the exact shape from `contract.json` (e.g. the `postgres` namespace's specific readonly fields). Tests need to construct contracts with arbitrary capability shapes, which don't fit the narrow type. The helper centralizes the structural cast in one named, documented place. - Removes per-test `as unknown as ReturnType` / `as unknown as TestContract` casts from `include-strategy.test.ts` (was 7) and `collection-dispatch.test.ts` (was 3) by routing through `withCapabilities`. - The pre-existing `withSingleQueryCapabilities` and the new `withEmittedSqlCapabilities` helpers in `collection-dispatch.test.ts` delegate to `withCapabilities`. Regression guards: - `collection-dispatch.test.ts` — `withEmittedSqlCapabilities` produces `{ sql: { jsonAgg: true }, postgres: { jsonAgg: true, lateral: true } }`. Asserts on MockRuntime's `executions.length`. - `integration/include.test.ts` — uses `getTestContract()` directly (whose capabilities are already in the emitted shape via the `postgres` namespace) against the dev Postgres. Asserts `runtime.executions.length === 1`. All 481 sql-orm-client tests pass. --- .../test/collection-dispatch.test.ts | 73 +++++++++++++++---- .../sql-orm-client/test/helpers.ts | 18 +++++ .../test/include-strategy.test.ts | 71 +++++++----------- .../test/integration/include.test.ts | 35 +++++++++ 4 files changed, 136 insertions(+), 61 deletions(-) diff --git a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts index 4f91bd1f6d..33928018b5 100644 --- a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts +++ b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts @@ -6,24 +6,35 @@ import type { IncludeExpr } from '../src/types'; import { emptyState } from '../src/types'; import { createCollectionFor } from './collection-fixtures'; import type { MockRuntime, TestContract } from './helpers'; -import { createMockRuntime, getTestContract } from './helpers'; +import { createMockRuntime, getTestContract, withCapabilities } from './helpers'; function withSingleQueryCapabilities(contract: TestContract): TestContract { - return { - ...contract, - capabilities: { - ...contract.capabilities, - [contract.targetFamily]: { - ...(contract.capabilities[contract.targetFamily] ?? {}), - jsonAgg: true, - }, - [contract.target]: { - ...(contract.capabilities[contract.target] ?? {}), - jsonAgg: true, - lateral: true, - }, + return withCapabilities(contract, { + ...contract.capabilities, + [contract.targetFamily]: { + ...(contract.capabilities[contract.targetFamily] ?? {}), + jsonAgg: true, + }, + [contract.target]: { + ...(contract.capabilities[contract.target] ?? {}), + jsonAgg: true, + lateral: true, }, - } as unknown as TestContract; + }); +} + +/** + * Mirrors the shape produced by the contract emitter: capability flags + * nested under the family + target namespaces, with no top-level entries. + * Used to assert "single-query path is selected for an emitted-shape + * contract" — the regression scenario the principled namespaced lookup + * was introduced to handle. + */ +function withEmittedSqlCapabilities(contract: TestContract): TestContract { + return withCapabilities(contract, { + sql: { jsonAgg: true, returning: true }, + postgres: { jsonAgg: true, lateral: true, returning: true }, + }); } function addConnection( @@ -78,6 +89,36 @@ describe('collection-dispatch', () => { expect(rows).toEqual([{ id: 1, name: 'Alice', email: 'alice@example.com' }]); }); + it('dispatchCollectionRows() depth-1 include with emitted-shape capabilities fires a single SQL execution (regression guard for namespaced capability lookup)', async () => { + // Guards against regressing the fix that taught `selectIncludeStrategy` + // to read capability flags from the contract's `targetFamily` and + // `target` namespaces. Prior to that fix, every emitted contract fell + // back to multi-query for nested includes — silently, because + // functional correctness was unaffected. This test fails fast if the + // regression returns: an emitted-shape contract should resolve a + // depth-1 include in one SQL execution, not two. + const contract = withEmittedSqlCapabilities(getTestContract()); + const { collection, runtime } = createCollectionFor('User', contract); + const scoped = collection.select('name').include('posts'); + runtime.setNextResults([ + [{ id: 1, name: 'Alice', posts: '[{"id":10,"title":"Post A","user_id":1,"views":3}]' }], + ]); + + const rows = await dispatchCollectionRows>({ + contract, + runtime, + state: scoped.state, + tableName: scoped.tableName, + modelName: scoped.modelName, + }).toArray(); + + expect(rows).toEqual([ + { name: 'Alice', posts: [{ id: 10, title: 'Post A', userId: 1, views: 3 }] }, + ]); + // The point of the test: 1 execution, not N+1. + expect(runtime.executions).toHaveLength(1); + }); + it('dispatchCollectionRows() single-query path returns empty rows and releases scope', async () => { const contract = withSingleQueryCapabilities(getTestContract()); const { collection, runtime } = createCollectionFor('User', contract); @@ -205,7 +246,7 @@ describe('collection-dispatch', () => { // Force multi-query strategy by clearing capabilities. Otherwise // the base test contract's postgres.lateral / postgres.jsonAgg // would route to single-query lateral. - const contract = { ...getTestContract(), capabilities: {} } as unknown as TestContract; + const contract = withCapabilities(getTestContract(), {}); const { collection, runtime } = createCollectionFor('User', contract); const scoped = collection.select('name').include('posts', (posts) => posts.select('title')); diff --git a/packages/3-extensions/sql-orm-client/test/helpers.ts b/packages/3-extensions/sql-orm-client/test/helpers.ts index aa1337ec9d..f376af2c08 100644 --- a/packages/3-extensions/sql-orm-client/test/helpers.ts +++ b/packages/3-extensions/sql-orm-client/test/helpers.ts @@ -24,6 +24,24 @@ export function getTestContract(): TestContract { return structuredClone(baseTestContract); } +/** + * Override the capabilities of a {@link TestContract} for a test scenario. + * + * The narrow `TestContract` type fixes capabilities to the exact shape + * found in `fixtures/generated/contract.json` (e.g. the `postgres` + * namespace's specific readonly fields). Tests need to construct + * contracts with arbitrary capability shapes — empty, only-jsonAgg, + * cross-namespace, etc. — that don't fit that narrow type. + * + * This helper centralizes the structural cast so call sites stay clean. + */ +export function withCapabilities( + contract: TestContract, + capabilities: Record>, +): TestContract { + return { ...contract, capabilities } as unknown as TestContract; +} + const testContext: ExecutionContext = createExecutionContext({ contract: baseTestContract, stack: createSqlExecutionStack({ diff --git a/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts b/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts index 7f4ef3c518..15c74eabfd 100644 --- a/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts +++ b/packages/3-extensions/sql-orm-client/test/include-strategy.test.ts @@ -1,40 +1,31 @@ import { describe, expect, it } from 'vitest'; import { selectIncludeStrategy } from '../src/include-strategy'; -import { getTestContract } from './helpers'; +import { getTestContract, withCapabilities } from './helpers'; // The default test contract has `target: 'postgres'`, `targetFamily: 'sql'`, // and capabilities populated under those two namespaces. The strategy -// selector reads only those namespaces, so override `capabilities` -// directly to drive each scenario. +// selector reads only those namespaces, so each test uses +// `withCapabilities(...)` to swap in the override the scenario needs. describe('selectIncludeStrategy', () => { it('returns multiQuery when include capabilities are absent', () => { - const contract = { - ...getTestContract(), - capabilities: {}, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), {}); expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); it('returns correlated when jsonAgg is enabled in the family namespace without lateral', () => { - const contract = { - ...getTestContract(), - capabilities: { - sql: { jsonAgg: true }, - }, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), { + sql: { jsonAgg: true }, + }); expect(selectIncludeStrategy(contract)).toBe('correlated'); }); it('returns lateral when both flags are enabled in the same namespace', () => { - const contract = { - ...getTestContract(), - capabilities: { - postgres: { jsonAgg: true, lateral: true }, - }, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), { + postgres: { jsonAgg: true, lateral: true }, + }); expect(selectIncludeStrategy(contract)).toBe('lateral'); }); @@ -42,13 +33,10 @@ describe('selectIncludeStrategy', () => { it('returns lateral when flags are split across family and target namespaces', () => { // Real-world shape: SQL family declares `jsonAgg`; the postgres // target adds `lateral` on top. - const contract = { - ...getTestContract(), - capabilities: { - sql: { jsonAgg: true }, - postgres: { lateral: true }, - }, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), { + sql: { jsonAgg: true }, + postgres: { lateral: true }, + }); expect(selectIncludeStrategy(contract)).toBe('lateral'); }); @@ -58,13 +46,10 @@ describe('selectIncludeStrategy', () => { // A `mongo: { lateral: true }` namespace must not enable lateral // on a postgres runtime — namespaces are scoped to the running // target/family. - const contract = { - ...getTestContract(), - capabilities: { - mongo: { jsonAgg: true, lateral: true }, - nonsense: { lateral: true }, - }, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), { + mongo: { jsonAgg: true, lateral: true }, + nonsense: { lateral: true }, + }); expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); @@ -72,23 +57,19 @@ describe('selectIncludeStrategy', () => { it('treats non-boolean capability values as missing', () => { // The Contract type declares capability values as `boolean`. Anything // else (string, object, undefined) is treated as not present. - const contract = { - ...getTestContract(), - capabilities: { - sql: { jsonAgg: 'yes' as unknown as boolean, lateral: true }, - }, - } as unknown as ReturnType; + // The cast on `'yes'` is deliberate — we're feeding an invalid value + // through a valid-typed contract to exercise the runtime check. + const contract = withCapabilities(getTestContract(), { + sql: { jsonAgg: 'yes' as unknown as boolean, lateral: true }, + }); expect(selectIncludeStrategy(contract)).toBe('multiQuery'); }); it('treats explicit `false` as not enabled', () => { - const contract = { - ...getTestContract(), - capabilities: { - sql: { jsonAgg: true, lateral: false }, - }, - } as unknown as ReturnType; + const contract = withCapabilities(getTestContract(), { + sql: { jsonAgg: true, lateral: false }, + }); expect(selectIncludeStrategy(contract)).toBe('correlated'); }); diff --git a/packages/3-extensions/sql-orm-client/test/integration/include.test.ts b/packages/3-extensions/sql-orm-client/test/integration/include.test.ts index 5b2de377ee..ff44583db2 100644 --- a/packages/3-extensions/sql-orm-client/test/integration/include.test.ts +++ b/packages/3-extensions/sql-orm-client/test/integration/include.test.ts @@ -67,6 +67,41 @@ type NumericPostField = import('../../src/types').NumericFieldNames< >; describe('integration/include', () => { + it( + 'depth-1 include against an emitted contract fires a single SQL execution (regression guard for namespaced capability lookup)', + async () => { + // Guards against regressing the fix that taught `selectIncludeStrategy` + // to read capability flags from the contract's `targetFamily` and + // `target` namespaces. The default `getTestContract()` carries + // `postgres: { lateral: true, jsonAgg: true, ... }` — the emitter's + // actual output shape. Prior to the fix, this exact test would fire + // 2 SQL queries instead of 1, against a real driver. + await withCollectionRuntime(async (runtime) => { + const users = createUsersCollection(runtime); + + await seedUsers(runtime, [{ id: 1, name: 'Alice', email: 'alice@example.com' }]); + await seedPosts(runtime, [{ id: 10, title: 'Post A', userId: 1, views: 100 }]); + + runtime.resetExecutions(); + const rows = await users.include('posts').all(); + + expect(rows).toEqual([ + { + id: 1, + name: 'Alice', + email: 'alice@example.com', + invitedById: null, + address: null, + posts: [{ id: 10, title: 'Post A', userId: 1, views: 100, embedding: null }], + }, + ]); + // The point of the test: 1 execution, not N+1. + expect(runtime.executions).toHaveLength(1); + }); + }, + timeouts.spinUpPpgDev, + ); + it( 'include() stitches one-to-many and one-to-one relations from real rows', async () => { From 5e32fabbdef5be9c2d70a8caa7f48b63df942488 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20Berg?= Date: Wed, 6 May 2026 15:48:22 +0200 Subject: [PATCH 3/4] test(sql-orm-client): preserve literal capability types in withCapabilities MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the `as unknown as TestContract` cast with a generic over `TCaps extends Record>`, returning `Omit & { readonly capabilities: TCaps }`. Capability-dependent type checks now see the caller's literal capability shape instead of the narrow `TestContract` slot. Addresses @aqrln's review note: "I'd make this helper produce an accurate contract types from day one if it's not too complex." Widens `contextForContract` and `createCollectionFor` in `collection-fixtures.ts` to accept `Contract` so the more precise return type flows through; the existing internal cast inside `contextForContract` already absorbs the structural gap. Drops the explicit `: TestContract` return annotations on the two thin `with…Capabilities` wrappers in `collection-dispatch.test.ts` so the inferred result keeps its literal capability shape. All 481 sql-orm-client tests pass. --- .../test/collection-dispatch.test.ts | 4 ++-- .../test/collection-fixtures.ts | 6 +++-- .../sql-orm-client/test/helpers.ts | 22 ++++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts index 33928018b5..6fb68d76d4 100644 --- a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts +++ b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts @@ -8,7 +8,7 @@ import { createCollectionFor } from './collection-fixtures'; import type { MockRuntime, TestContract } from './helpers'; import { createMockRuntime, getTestContract, withCapabilities } from './helpers'; -function withSingleQueryCapabilities(contract: TestContract): TestContract { +function withSingleQueryCapabilities(contract: TestContract) { return withCapabilities(contract, { ...contract.capabilities, [contract.targetFamily]: { @@ -30,7 +30,7 @@ function withSingleQueryCapabilities(contract: TestContract): TestContract { * contract" — the regression scenario the principled namespaced lookup * was introduced to handle. */ -function withEmittedSqlCapabilities(contract: TestContract): TestContract { +function withEmittedSqlCapabilities(contract: TestContract) { return withCapabilities(contract, { sql: { jsonAgg: true, returning: true }, postgres: { jsonAgg: true, lateral: true, returning: true }, diff --git a/packages/3-extensions/sql-orm-client/test/collection-fixtures.ts b/packages/3-extensions/sql-orm-client/test/collection-fixtures.ts index dbce9fb938..827006a923 100644 --- a/packages/3-extensions/sql-orm-client/test/collection-fixtures.ts +++ b/packages/3-extensions/sql-orm-client/test/collection-fixtures.ts @@ -1,3 +1,5 @@ +import type { Contract } from '@prisma-next/contract/types'; +import type { SqlStorage } from '@prisma-next/sql-contract/types'; import type { ExecutionContext } from '@prisma-next/sql-relational-core/query-lane-context'; import { Collection } from '../src/collection'; import type { MockRuntime, TestContract } from './helpers'; @@ -7,7 +9,7 @@ export type TestModelName = Extract; export const baseContract = getTestContract(); -function contextForContract(contract: TestContract): ExecutionContext { +function contextForContract(contract: Contract): ExecutionContext { const base = getTestContext(); if (contract === baseContract) return base; return { ...base, contract } as ExecutionContext; @@ -15,7 +17,7 @@ function contextForContract(contract: TestContract): ExecutionContext( modelName: ModelName, - contract: TestContract = baseContract, + contract: Contract = baseContract, ): { collection: Collection; runtime: MockRuntime; diff --git a/packages/3-extensions/sql-orm-client/test/helpers.ts b/packages/3-extensions/sql-orm-client/test/helpers.ts index f376af2c08..2e63fe280e 100644 --- a/packages/3-extensions/sql-orm-client/test/helpers.ts +++ b/packages/3-extensions/sql-orm-client/test/helpers.ts @@ -27,19 +27,21 @@ export function getTestContract(): TestContract { /** * Override the capabilities of a {@link TestContract} for a test scenario. * - * The narrow `TestContract` type fixes capabilities to the exact shape - * found in `fixtures/generated/contract.json` (e.g. the `postgres` - * namespace's specific readonly fields). Tests need to construct - * contracts with arbitrary capability shapes — empty, only-jsonAgg, - * cross-namespace, etc. — that don't fit that narrow type. + * The narrow `TestContract` type fixes `capabilities` to the literal shape + * generated for `fixtures/generated/contract.json`. Tests need contracts + * with arbitrary capability shapes — empty, only-jsonAgg, cross-namespace, + * etc. — and want the override's literal types preserved so capability- + * dependent type checks remain meaningful. * - * This helper centralizes the structural cast so call sites stay clean. + * The result widens `TestContract`'s `capabilities` slot to the caller's + * `TCaps`, which the framework `Contract` interface already permits + * (`capabilities: Record>`). */ -export function withCapabilities( +export function withCapabilities>>( contract: TestContract, - capabilities: Record>, -): TestContract { - return { ...contract, capabilities } as unknown as TestContract; + capabilities: TCaps, +): Omit & { readonly capabilities: TCaps } { + return { ...contract, capabilities }; } const testContext: ExecutionContext = createExecutionContext({ From 9b4ad815695b79967176ec4998295cff0759ec1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A6var=20Berg?= Date: Thu, 7 May 2026 13:07:16 +0200 Subject: [PATCH 4/4] test(sql-orm-client): force multi-query strategy in empty-parent test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `multi-query path handles empty parent result sets` was using the bare `getTestContract()`. That contract has `postgres.lateral` and `postgres.jsonAgg` set in the emitted shape, so post-fix the test routes through the single-query lateral path instead of multi-query — leaving the empty-parent early return at `collection-dispatch.ts:209` unhit and dropping package branch coverage just under the 93% gate. Pre-fix this never showed up because `selectIncludeStrategy` could not read namespaced capabilities and silently fell back to multi-query for every emitted contract — so this test was hitting the multi-query empty-parent branch by accident. Use the same `withCapabilities(getTestContract(), {})` trick the sibling test on line 245 already uses to force the multi-query path. The test now actually exercises what its name describes. --- .../sql-orm-client/test/collection-dispatch.test.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts index 6fb68d76d4..fa2c796842 100644 --- a/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts +++ b/packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts @@ -288,13 +288,18 @@ describe('collection-dispatch', () => { }); it('dispatchCollectionRows() multi-query path handles empty parent result sets', async () => { - const { collection, runtime } = createCollectionFor('User'); + // Force multi-query strategy so the empty-parent early return inside + // `dispatchWithMultiQueryIncludes` is actually exercised. The base + // contract's postgres.lateral / postgres.jsonAgg would otherwise + // route to single-query lateral. + const contract = withCapabilities(getTestContract(), {}); + const { collection, runtime } = createCollectionFor('User', contract); const scoped = collection.include('posts'); runtime.setNextResults([[]]); const rows = await dispatchCollectionRows>({ - contract: collection.ctx.context.contract, + contract, runtime, state: scoped.state, tableName: scoped.tableName,