Skip to content

Commit c600e7f

Browse files
author
Sævar Berg
committed
test(sql-orm-client): regression guards for namespaced capability
lookup; centralize the structural cast in a `withCapabilities` helper 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<typeof getTestContract>` / `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.
1 parent 0115856 commit c600e7f

4 files changed

Lines changed: 136 additions & 61 deletions

File tree

packages/3-extensions/sql-orm-client/test/collection-dispatch.test.ts

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -6,24 +6,35 @@ import type { IncludeExpr } from '../src/types';
66
import { emptyState } from '../src/types';
77
import { createCollectionFor } from './collection-fixtures';
88
import type { MockRuntime, TestContract } from './helpers';
9-
import { createMockRuntime, getTestContract } from './helpers';
9+
import { createMockRuntime, getTestContract, withCapabilities } from './helpers';
1010

1111
function withSingleQueryCapabilities(contract: TestContract): TestContract {
12-
return {
13-
...contract,
14-
capabilities: {
15-
...contract.capabilities,
16-
[contract.targetFamily]: {
17-
...(contract.capabilities[contract.targetFamily] ?? {}),
18-
jsonAgg: true,
19-
},
20-
[contract.target]: {
21-
...(contract.capabilities[contract.target] ?? {}),
22-
jsonAgg: true,
23-
lateral: true,
24-
},
12+
return withCapabilities(contract, {
13+
...contract.capabilities,
14+
[contract.targetFamily]: {
15+
...(contract.capabilities[contract.targetFamily] ?? {}),
16+
jsonAgg: true,
17+
},
18+
[contract.target]: {
19+
...(contract.capabilities[contract.target] ?? {}),
20+
jsonAgg: true,
21+
lateral: true,
2522
},
26-
} as unknown as TestContract;
23+
});
24+
}
25+
26+
/**
27+
* Mirrors the shape produced by the contract emitter: capability flags
28+
* nested under the family + target namespaces, with no top-level entries.
29+
* Used to assert "single-query path is selected for an emitted-shape
30+
* contract" — the regression scenario the principled namespaced lookup
31+
* was introduced to handle.
32+
*/
33+
function withEmittedSqlCapabilities(contract: TestContract): TestContract {
34+
return withCapabilities(contract, {
35+
sql: { jsonAgg: true, returning: true },
36+
postgres: { jsonAgg: true, lateral: true, returning: true },
37+
});
2738
}
2839

2940
function addConnection(
@@ -78,6 +89,36 @@ describe('collection-dispatch', () => {
7889
expect(rows).toEqual([{ id: 1, name: 'Alice', email: 'alice@example.com' }]);
7990
});
8091

92+
it('dispatchCollectionRows() depth-1 include with emitted-shape capabilities fires a single SQL execution (regression guard for namespaced capability lookup)', async () => {
93+
// Guards against regressing the fix that taught `selectIncludeStrategy`
94+
// to read capability flags from the contract's `targetFamily` and
95+
// `target` namespaces. Prior to that fix, every emitted contract fell
96+
// back to multi-query for nested includes — silently, because
97+
// functional correctness was unaffected. This test fails fast if the
98+
// regression returns: an emitted-shape contract should resolve a
99+
// depth-1 include in one SQL execution, not two.
100+
const contract = withEmittedSqlCapabilities(getTestContract());
101+
const { collection, runtime } = createCollectionFor('User', contract);
102+
const scoped = collection.select('name').include('posts');
103+
runtime.setNextResults([
104+
[{ id: 1, name: 'Alice', posts: '[{"id":10,"title":"Post A","user_id":1,"views":3}]' }],
105+
]);
106+
107+
const rows = await dispatchCollectionRows<Record<string, unknown>>({
108+
contract,
109+
runtime,
110+
state: scoped.state,
111+
tableName: scoped.tableName,
112+
modelName: scoped.modelName,
113+
}).toArray();
114+
115+
expect(rows).toEqual([
116+
{ name: 'Alice', posts: [{ id: 10, title: 'Post A', userId: 1, views: 3 }] },
117+
]);
118+
// The point of the test: 1 execution, not N+1.
119+
expect(runtime.executions).toHaveLength(1);
120+
});
121+
81122
it('dispatchCollectionRows() single-query path returns empty rows and releases scope', async () => {
82123
const contract = withSingleQueryCapabilities(getTestContract());
83124
const { collection, runtime } = createCollectionFor('User', contract);
@@ -205,7 +246,7 @@ describe('collection-dispatch', () => {
205246
// Force multi-query strategy by clearing capabilities. Otherwise
206247
// the base test contract's postgres.lateral / postgres.jsonAgg
207248
// would route to single-query lateral.
208-
const contract = { ...getTestContract(), capabilities: {} } as unknown as TestContract;
249+
const contract = withCapabilities(getTestContract(), {});
209250
const { collection, runtime } = createCollectionFor('User', contract);
210251
const scoped = collection.select('name').include('posts', (posts) => posts.select('title'));
211252

packages/3-extensions/sql-orm-client/test/helpers.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,24 @@ export function getTestContract(): TestContract {
2424
return structuredClone(baseTestContract);
2525
}
2626

27+
/**
28+
* Override the capabilities of a {@link TestContract} for a test scenario.
29+
*
30+
* The narrow `TestContract` type fixes capabilities to the exact shape
31+
* found in `fixtures/generated/contract.json` (e.g. the `postgres`
32+
* namespace's specific readonly fields). Tests need to construct
33+
* contracts with arbitrary capability shapes — empty, only-jsonAgg,
34+
* cross-namespace, etc. — that don't fit that narrow type.
35+
*
36+
* This helper centralizes the structural cast so call sites stay clean.
37+
*/
38+
export function withCapabilities(
39+
contract: TestContract,
40+
capabilities: Record<string, Record<string, boolean>>,
41+
): TestContract {
42+
return { ...contract, capabilities } as unknown as TestContract;
43+
}
44+
2745
const testContext: ExecutionContext<TestContract> = createExecutionContext({
2846
contract: baseTestContract,
2947
stack: createSqlExecutionStack({

packages/3-extensions/sql-orm-client/test/include-strategy.test.ts

Lines changed: 26 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,54 +1,42 @@
11
import { describe, expect, it } from 'vitest';
22
import { selectIncludeStrategy } from '../src/include-strategy';
3-
import { getTestContract } from './helpers';
3+
import { getTestContract, withCapabilities } from './helpers';
44

55
// The default test contract has `target: 'postgres'`, `targetFamily: 'sql'`,
66
// and capabilities populated under those two namespaces. The strategy
7-
// selector reads only those namespaces, so override `capabilities`
8-
// directly to drive each scenario.
7+
// selector reads only those namespaces, so each test uses
8+
// `withCapabilities(...)` to swap in the override the scenario needs.
99

1010
describe('selectIncludeStrategy', () => {
1111
it('returns multiQuery when include capabilities are absent', () => {
12-
const contract = {
13-
...getTestContract(),
14-
capabilities: {},
15-
} as unknown as ReturnType<typeof getTestContract>;
12+
const contract = withCapabilities(getTestContract(), {});
1613

1714
expect(selectIncludeStrategy(contract)).toBe('multiQuery');
1815
});
1916

2017
it('returns correlated when jsonAgg is enabled in the family namespace without lateral', () => {
21-
const contract = {
22-
...getTestContract(),
23-
capabilities: {
24-
sql: { jsonAgg: true },
25-
},
26-
} as unknown as ReturnType<typeof getTestContract>;
18+
const contract = withCapabilities(getTestContract(), {
19+
sql: { jsonAgg: true },
20+
});
2721

2822
expect(selectIncludeStrategy(contract)).toBe('correlated');
2923
});
3024

3125
it('returns lateral when both flags are enabled in the same namespace', () => {
32-
const contract = {
33-
...getTestContract(),
34-
capabilities: {
35-
postgres: { jsonAgg: true, lateral: true },
36-
},
37-
} as unknown as ReturnType<typeof getTestContract>;
26+
const contract = withCapabilities(getTestContract(), {
27+
postgres: { jsonAgg: true, lateral: true },
28+
});
3829

3930
expect(selectIncludeStrategy(contract)).toBe('lateral');
4031
});
4132

4233
it('returns lateral when flags are split across family and target namespaces', () => {
4334
// Real-world shape: SQL family declares `jsonAgg`; the postgres
4435
// target adds `lateral` on top.
45-
const contract = {
46-
...getTestContract(),
47-
capabilities: {
48-
sql: { jsonAgg: true },
49-
postgres: { lateral: true },
50-
},
51-
} as unknown as ReturnType<typeof getTestContract>;
36+
const contract = withCapabilities(getTestContract(), {
37+
sql: { jsonAgg: true },
38+
postgres: { lateral: true },
39+
});
5240

5341
expect(selectIncludeStrategy(contract)).toBe('lateral');
5442
});
@@ -58,37 +46,30 @@ describe('selectIncludeStrategy', () => {
5846
// A `mongo: { lateral: true }` namespace must not enable lateral
5947
// on a postgres runtime — namespaces are scoped to the running
6048
// target/family.
61-
const contract = {
62-
...getTestContract(),
63-
capabilities: {
64-
mongo: { jsonAgg: true, lateral: true },
65-
nonsense: { lateral: true },
66-
},
67-
} as unknown as ReturnType<typeof getTestContract>;
49+
const contract = withCapabilities(getTestContract(), {
50+
mongo: { jsonAgg: true, lateral: true },
51+
nonsense: { lateral: true },
52+
});
6853

6954
expect(selectIncludeStrategy(contract)).toBe('multiQuery');
7055
});
7156

7257
it('treats non-boolean capability values as missing', () => {
7358
// The Contract type declares capability values as `boolean`. Anything
7459
// else (string, object, undefined) is treated as not present.
75-
const contract = {
76-
...getTestContract(),
77-
capabilities: {
78-
sql: { jsonAgg: 'yes' as unknown as boolean, lateral: true },
79-
},
80-
} as unknown as ReturnType<typeof getTestContract>;
60+
// The cast on `'yes'` is deliberate — we're feeding an invalid value
61+
// through a valid-typed contract to exercise the runtime check.
62+
const contract = withCapabilities(getTestContract(), {
63+
sql: { jsonAgg: 'yes' as unknown as boolean, lateral: true },
64+
});
8165

8266
expect(selectIncludeStrategy(contract)).toBe('multiQuery');
8367
});
8468

8569
it('treats explicit `false` as not enabled', () => {
86-
const contract = {
87-
...getTestContract(),
88-
capabilities: {
89-
sql: { jsonAgg: true, lateral: false },
90-
},
91-
} as unknown as ReturnType<typeof getTestContract>;
70+
const contract = withCapabilities(getTestContract(), {
71+
sql: { jsonAgg: true, lateral: false },
72+
});
9273

9374
expect(selectIncludeStrategy(contract)).toBe('correlated');
9475
});

packages/3-extensions/sql-orm-client/test/integration/include.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,41 @@ type NumericPostField = import('../../src/types').NumericFieldNames<
6767
>;
6868

6969
describe('integration/include', () => {
70+
it(
71+
'depth-1 include against an emitted contract fires a single SQL execution (regression guard for namespaced capability lookup)',
72+
async () => {
73+
// Guards against regressing the fix that taught `selectIncludeStrategy`
74+
// to read capability flags from the contract's `targetFamily` and
75+
// `target` namespaces. The default `getTestContract()` carries
76+
// `postgres: { lateral: true, jsonAgg: true, ... }` — the emitter's
77+
// actual output shape. Prior to the fix, this exact test would fire
78+
// 2 SQL queries instead of 1, against a real driver.
79+
await withCollectionRuntime(async (runtime) => {
80+
const users = createUsersCollection(runtime);
81+
82+
await seedUsers(runtime, [{ id: 1, name: 'Alice', email: 'alice@example.com' }]);
83+
await seedPosts(runtime, [{ id: 10, title: 'Post A', userId: 1, views: 100 }]);
84+
85+
runtime.resetExecutions();
86+
const rows = await users.include('posts').all();
87+
88+
expect(rows).toEqual([
89+
{
90+
id: 1,
91+
name: 'Alice',
92+
email: 'alice@example.com',
93+
invitedById: null,
94+
address: null,
95+
posts: [{ id: 10, title: 'Post A', userId: 1, views: 100, embedding: null }],
96+
},
97+
]);
98+
// The point of the test: 1 execution, not N+1.
99+
expect(runtime.executions).toHaveLength(1);
100+
});
101+
},
102+
timeouts.spinUpPpgDev,
103+
);
104+
70105
it(
71106
'include() stitches one-to-many and one-to-one relations from real rows',
72107
async () => {

0 commit comments

Comments
 (0)