Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 36 additions & 14 deletions packages/3-extensions/sql-orm-client/src/include-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<SqlStorage>): IncludeStrategy {
const capabilities = contract.capabilities as Record<string, unknown> | 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';
Expand All @@ -19,15 +38,18 @@ export function selectIncludeStrategy(contract: Contract<SqlStorage>): 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<string, unknown>;
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<SqlStorage>, flag: string): boolean {
return (
contract.capabilities[contract.targetFamily]?.[flag] === true ||
contract.capabilities[contract.target]?.[flag] === true
Comment on lines +52 to +53
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is an old mistake in the emission process: capability flags should not be namespaced, they're either present or not, and we don't care which framework component contributes them. Your fix is more correct than the current implementation, but still working around this core mistake.

);
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +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';

function withSingleQueryCapabilities(contract: TestContract): TestContract {
return {
...contract,
capabilities: {
...contract.capabilities,
lateral: { enabled: true },
jsonAgg: { enabled: true },
import { createMockRuntime, getTestContract, withCapabilities } from './helpers';

function withSingleQueryCapabilities(contract: TestContract) {
return withCapabilities(contract, {
...contract.capabilities,
[contract.targetFamily]: {
...(contract.capabilities[contract.targetFamily] ?? {}),
jsonAgg: true,
},
} as unknown as TestContract;
[contract.target]: {
...(contract.capabilities[contract.target] ?? {}),
jsonAgg: true,
lateral: true,
},
});
}

/**
* 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) {
return withCapabilities(contract, {
sql: { jsonAgg: true, returning: true },
postgres: { jsonAgg: true, lateral: true, returning: true },
});
}

function addConnection(
Expand Down Expand Up @@ -71,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<Record<string, unknown>>({
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);
Expand Down Expand Up @@ -195,7 +243,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 = withCapabilities(getTestContract(), {});
const { collection, runtime } = createCollectionFor('User', contract);
const scoped = collection.select('name').include('posts', (posts) => posts.select('title'));

Expand Down Expand Up @@ -237,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<Record<string, unknown>>({
contract: collection.ctx.context.contract,
contract,
runtime,
state: scoped.state,
tableName: scoped.tableName,
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -7,15 +9,15 @@ export type TestModelName = Extract<keyof TestContract['models'], string>;

export const baseContract = getTestContract();

function contextForContract(contract: TestContract): ExecutionContext<TestContract> {
function contextForContract(contract: Contract<SqlStorage>): ExecutionContext<TestContract> {
const base = getTestContext();
if (contract === baseContract) return base;
return { ...base, contract } as ExecutionContext<TestContract>;
}

export function createCollectionFor<ModelName extends TestModelName>(
modelName: ModelName,
contract: TestContract = baseContract,
contract: Contract<SqlStorage> = baseContract,
): {
collection: Collection<TestContract, ModelName>;
runtime: MockRuntime;
Expand Down
20 changes: 20 additions & 0 deletions packages/3-extensions/sql-orm-client/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,26 @@ 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 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.
*
* The result widens `TestContract`'s `capabilities` slot to the caller's
* `TCaps`, which the framework `Contract` interface already permits
* (`capabilities: Record<string, Record<string, boolean>>`).
*/
export function withCapabilities<TCaps extends Record<string, Record<string, boolean>>>(
contract: TestContract,
capabilities: TCaps,
): Omit<TestContract, 'capabilities'> & { readonly capabilities: TCaps } {
return { ...contract, capabilities };
}

const testContext: ExecutionContext<TestContract> = createExecutionContext({
contract: baseTestContract,
stack: createSqlExecutionStack({
Expand Down
112 changes: 53 additions & 59 deletions packages/3-extensions/sql-orm-client/test/include-strategy.test.ts
Original file line number Diff line number Diff line change
@@ -1,82 +1,76 @@
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 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();
const strategy = selectIncludeStrategy(contract);
const contract = withCapabilities(getTestContract(), {});

expect(selectIncludeStrategy(contract)).toBe('multiQuery');
});

it('returns correlated when jsonAgg is enabled in the family namespace without lateral', () => {
const contract = withCapabilities(getTestContract(), {
sql: { jsonAgg: true },
});

expect(strategy).toBe('multiQuery');
expect(selectIncludeStrategy(contract)).toBe('correlated');
});

it('returns correlated when jsonAgg is enabled without lateral', () => {
const contract = {
...getTestContract(),
capabilities: {
jsonAgg: {
enabled: true,
},
},
};
it('returns lateral when both flags are enabled in the same namespace', () => {
const contract = withCapabilities(getTestContract(), {
postgres: { jsonAgg: true, lateral: true },
});

const strategy = selectIncludeStrategy(contract);
expect(strategy).toBe('correlated');
expect(selectIncludeStrategy(contract)).toBe('lateral');
});

it('returns lateral when both lateral and jsonAgg are enabled', () => {
const contract = {
...getTestContract(),
capabilities: {
lateral: {
enabled: true,
},
jsonAgg: {
enabled: true,
},
},
};
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 = withCapabilities(getTestContract(), {
sql: { jsonAgg: true },
postgres: { lateral: true },
});

const strategy = selectIncludeStrategy(contract);
expect(strategy).toBe('lateral');
expect(selectIncludeStrategy(contract)).toBe('lateral');
});

it('reads object capabilities via enabled flag', () => {
const contract = {
...getTestContract(),
capabilities: {
lateral: { enabled: true },
jsonAgg: { enabled: false, fallback: true },
},
};
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 = withCapabilities(getTestContract(), {
mongo: { jsonAgg: true, lateral: true },
nonsense: { lateral: true },
});

const strategy = selectIncludeStrategy(contract);
expect(strategy).toBe('lateral');
expect(selectIncludeStrategy(contract)).toBe('multiQuery');
});

it('accepts top-level boolean capability flags', () => {
const contract = {
...getTestContract(),
capabilities: {
lateral: true,
jsonAgg: true,
},
} as unknown as ReturnType<typeof getTestContract>;
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.
// 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 },
});

const strategy = selectIncludeStrategy(contract);
expect(strategy).toBe('lateral');
expect(selectIncludeStrategy(contract)).toBe('multiQuery');
});

it('ignores non-boolean, non-object capability values', () => {
const contract = {
...getTestContract(),
capabilities: {
lateral: 'yes',
jsonAgg: true,
},
} as unknown as ReturnType<typeof getTestContract>;
it('treats explicit `false` as not enabled', () => {
const contract = withCapabilities(getTestContract(), {
sql: { jsonAgg: true, lateral: false },
});

const strategy = selectIncludeStrategy(contract);
expect(strategy).toBe('correlated');
expect(selectIncludeStrategy(contract)).toBe('correlated');
});
});
Loading
Loading