Skip to content

Commit d89aeac

Browse files
committed
test(sql-orm-client): M:N junction filter integration tests (some/none/every)
Add PGlite integration tests for User.tags M:N filter operators. Tests seed users/tags/junction rows via existing runtime-helpers and assert the exact filtered user set as whole rows (toEqual), covering: - some: users with ≥1 matching tag; multiple users; single match - none: users with no matching tag (including tag-less users) - every: all tags match + vacuous case (no-tags user qualifies) + partial-match exclusion - empty-match edge: some/none/every against a predicate no tag satisfies - implicit selection: one test without .select asserts full default row shape Signed-off-by: Alexey Orlenko's AI Agent <robot@aqrln.net>
1 parent 31d3afb commit d89aeac

1 file changed

Lines changed: 363 additions & 0 deletions

File tree

Lines changed: 363 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,363 @@
1+
// Integration coverage for M:N relation filters via junction EXISTS.
2+
//
3+
// `User.tags` is a many-to-many relation to `Tag` through the `user_tags`
4+
// junction table. `.some`/`.none`/`.every` on M:N relations emit correlated
5+
// EXISTS/NOT EXISTS subqueries against the junction. These tests prove
6+
// end-to-end correctness against a real database.
7+
//
8+
// Test data shape:
9+
//
10+
// User(id, name, email, invitedById?, address?)
11+
// tags: N:M Tag through user_tags (via user_id / tag_id)
12+
//
13+
// Tag(id: text, name: text)
14+
//
15+
// UserTag(userId, tagId) — junction
16+
//
17+
// Standard:
18+
// 1. Whole-row toEqual assertions on the exact filtered user set.
19+
// 2. Explicit .select() used in most tests.
20+
// 3. At least one test uses implicit/default selection (no .select()).
21+
22+
import { describe, expect, it } from 'vitest';
23+
import { createUsersCollection, timeouts, withCollectionRuntime } from './integration-helpers';
24+
import { seedTags, seedUsers, seedUserTags } from './runtime-helpers';
25+
26+
const TAG_RUST = 'tag-rust';
27+
const TAG_TS = 'tag-typescript';
28+
const TAG_DB = 'tag-database';
29+
30+
describe('integration/mn-filter', () => {
31+
// ===========================================================================
32+
// some — users having ≥1 tag matching the predicate.
33+
// ===========================================================================
34+
35+
it(
36+
'some: returns only users that have at least one matching tag (explicit select, whole-row toEqual)',
37+
async () => {
38+
await withCollectionRuntime(async (runtime) => {
39+
const users = createUsersCollection(runtime);
40+
41+
await seedUsers(runtime, [
42+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
43+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
44+
{ id: 3, name: 'Cara', email: 'cara@example.com' },
45+
]);
46+
await seedTags(runtime, [
47+
{ id: TAG_RUST, name: 'Rust' },
48+
{ id: TAG_TS, name: 'TypeScript' },
49+
]);
50+
// Alice: Rust + TypeScript, Bob: TypeScript only, Cara: no tags.
51+
await seedUserTags(runtime, [
52+
{ userId: 1, tagId: TAG_RUST },
53+
{ userId: 1, tagId: TAG_TS },
54+
{ userId: 2, tagId: TAG_TS },
55+
]);
56+
57+
const rows = await users
58+
.select('id', 'name')
59+
.where((u) => u.tags.some((t) => t.name.eq('Rust')))
60+
.orderBy((u) => u.id.asc())
61+
.all();
62+
63+
// Only Alice has Rust.
64+
expect(rows).toEqual([{ id: 1, name: 'Alice' }]);
65+
});
66+
},
67+
timeouts.spinUpPpgDev,
68+
);
69+
70+
it(
71+
'some: multiple users each having the matching tag are all returned (explicit select)',
72+
async () => {
73+
await withCollectionRuntime(async (runtime) => {
74+
const users = createUsersCollection(runtime);
75+
76+
await seedUsers(runtime, [
77+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
78+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
79+
{ id: 3, name: 'Cara', email: 'cara@example.com' },
80+
]);
81+
await seedTags(runtime, [
82+
{ id: TAG_TS, name: 'TypeScript' },
83+
{ id: TAG_DB, name: 'Database' },
84+
]);
85+
// Alice and Bob both have TypeScript; Cara only has Database.
86+
await seedUserTags(runtime, [
87+
{ userId: 1, tagId: TAG_TS },
88+
{ userId: 2, tagId: TAG_TS },
89+
{ userId: 3, tagId: TAG_DB },
90+
]);
91+
92+
const rows = await users
93+
.select('id', 'name')
94+
.where((u) => u.tags.some((t) => t.name.eq('TypeScript')))
95+
.orderBy((u) => u.id.asc())
96+
.all();
97+
98+
expect(rows).toEqual([
99+
{ id: 1, name: 'Alice' },
100+
{ id: 2, name: 'Bob' },
101+
]);
102+
});
103+
},
104+
timeouts.spinUpPpgDev,
105+
);
106+
107+
// ===========================================================================
108+
// none — users with no tag matching the predicate.
109+
// ===========================================================================
110+
111+
it(
112+
'none: returns only users with no tag matching the predicate (explicit select, whole-row toEqual)',
113+
async () => {
114+
await withCollectionRuntime(async (runtime) => {
115+
const users = createUsersCollection(runtime);
116+
117+
await seedUsers(runtime, [
118+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
119+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
120+
{ id: 3, name: 'Cara', email: 'cara@example.com' },
121+
]);
122+
await seedTags(runtime, [
123+
{ id: TAG_RUST, name: 'Rust' },
124+
{ id: TAG_TS, name: 'TypeScript' },
125+
]);
126+
// Alice: Rust only, Bob: TypeScript only, Cara: no tags.
127+
await seedUserTags(runtime, [
128+
{ userId: 1, tagId: TAG_RUST },
129+
{ userId: 2, tagId: TAG_TS },
130+
]);
131+
132+
const rows = await users
133+
.select('id', 'name')
134+
.where((u) => u.tags.none((t) => t.name.eq('Rust')))
135+
.orderBy((u) => u.id.asc())
136+
.all();
137+
138+
// Bob has no Rust tag; Cara has no tags at all (also satisfies none).
139+
expect(rows).toEqual([
140+
{ id: 2, name: 'Bob' },
141+
{ id: 3, name: 'Cara' },
142+
]);
143+
});
144+
},
145+
timeouts.spinUpPpgDev,
146+
);
147+
148+
// ===========================================================================
149+
// every — users all of whose tags match the predicate, including vacuous
150+
// case (user with no tags satisfies every) and exclusion of partial match.
151+
// ===========================================================================
152+
153+
it(
154+
'every: returns users whose tags all match the predicate, excludes partial match (explicit select)',
155+
async () => {
156+
await withCollectionRuntime(async (runtime) => {
157+
const users = createUsersCollection(runtime);
158+
159+
await seedUsers(runtime, [
160+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
161+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
162+
{ id: 3, name: 'Cara', email: 'cara@example.com' },
163+
]);
164+
await seedTags(runtime, [
165+
{ id: TAG_RUST, name: 'Rust' },
166+
{ id: TAG_TS, name: 'TypeScript' },
167+
]);
168+
// Alice: Rust only — all her tags are Rust → qualifies.
169+
// Bob: Rust + TypeScript — not all tags are Rust → excluded.
170+
// Cara: no tags — vacuously true → qualifies.
171+
await seedUserTags(runtime, [
172+
{ userId: 1, tagId: TAG_RUST },
173+
{ userId: 2, tagId: TAG_RUST },
174+
{ userId: 2, tagId: TAG_TS },
175+
]);
176+
177+
const rows = await users
178+
.select('id', 'name')
179+
.where((u) => u.tags.every((t) => t.name.eq('Rust')))
180+
.orderBy((u) => u.id.asc())
181+
.all();
182+
183+
// Alice: qualifies (only Rust). Cara: qualifies (vacuous). Bob: excluded.
184+
expect(rows).toEqual([
185+
{ id: 1, name: 'Alice' },
186+
{ id: 3, name: 'Cara' },
187+
]);
188+
});
189+
},
190+
timeouts.spinUpPpgDev,
191+
);
192+
193+
it(
194+
'every: vacuous case — user with no tags satisfies every (explicit select, isolated)',
195+
async () => {
196+
await withCollectionRuntime(async (runtime) => {
197+
const users = createUsersCollection(runtime);
198+
199+
await seedUsers(runtime, [
200+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
201+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
202+
]);
203+
await seedTags(runtime, [{ id: TAG_TS, name: 'TypeScript' }]);
204+
// Alice has TypeScript; Bob has no tags.
205+
await seedUserTags(runtime, [{ userId: 1, tagId: TAG_TS }]);
206+
207+
const rows = await users
208+
.select('id', 'name')
209+
.where((u) => u.tags.every((t) => t.name.eq('Rust')))
210+
.orderBy((u) => u.id.asc())
211+
.all();
212+
213+
// Alice has TypeScript which is NOT Rust → excluded.
214+
// Bob has no tags → vacuously satisfies every → included.
215+
expect(rows).toEqual([{ id: 2, name: 'Bob' }]);
216+
});
217+
},
218+
timeouts.spinUpPpgDev,
219+
);
220+
221+
// ===========================================================================
222+
// Implicit/default selection (standard requirement: ≥1 test without .select).
223+
// ===========================================================================
224+
225+
it(
226+
'some: no .select returns full default user row shape (implicit selection)',
227+
async () => {
228+
await withCollectionRuntime(async (runtime) => {
229+
const users = createUsersCollection(runtime);
230+
231+
await seedUsers(runtime, [
232+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
233+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
234+
]);
235+
await seedTags(runtime, [{ id: TAG_DB, name: 'Database' }]);
236+
// Only Alice has Database.
237+
await seedUserTags(runtime, [{ userId: 1, tagId: TAG_DB }]);
238+
239+
const rows = await users
240+
.where((u) => u.tags.some((t) => t.name.eq('Database')))
241+
.orderBy((u) => u.id.asc())
242+
.all();
243+
244+
// Full User row shape for Alice only.
245+
expect(rows).toEqual([
246+
{
247+
id: 1,
248+
name: 'Alice',
249+
email: 'alice@example.com',
250+
invitedById: null,
251+
address: null,
252+
},
253+
]);
254+
});
255+
},
256+
timeouts.spinUpPpgDev,
257+
);
258+
259+
// ===========================================================================
260+
// Empty-match edge — predicate that no tag satisfies.
261+
// ===========================================================================
262+
263+
it(
264+
'some with no matching tag returns empty result set (explicit select)',
265+
async () => {
266+
await withCollectionRuntime(async (runtime) => {
267+
const users = createUsersCollection(runtime);
268+
269+
await seedUsers(runtime, [
270+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
271+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
272+
]);
273+
await seedTags(runtime, [
274+
{ id: TAG_RUST, name: 'Rust' },
275+
{ id: TAG_TS, name: 'TypeScript' },
276+
]);
277+
await seedUserTags(runtime, [
278+
{ userId: 1, tagId: TAG_RUST },
279+
{ userId: 2, tagId: TAG_TS },
280+
]);
281+
282+
// 'Go' tag does not exist at all — some returns no users.
283+
const rows = await users
284+
.select('id', 'name')
285+
.where((u) => u.tags.some((t) => t.name.eq('Go')))
286+
.all();
287+
288+
expect(rows).toEqual([]);
289+
});
290+
},
291+
timeouts.spinUpPpgDev,
292+
);
293+
294+
it(
295+
'none with no matching tag (all users pass) returns all users (explicit select)',
296+
async () => {
297+
await withCollectionRuntime(async (runtime) => {
298+
const users = createUsersCollection(runtime);
299+
300+
await seedUsers(runtime, [
301+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
302+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
303+
]);
304+
await seedTags(runtime, [
305+
{ id: TAG_RUST, name: 'Rust' },
306+
{ id: TAG_TS, name: 'TypeScript' },
307+
]);
308+
// Neither user has a 'Go' tag.
309+
await seedUserTags(runtime, [
310+
{ userId: 1, tagId: TAG_RUST },
311+
{ userId: 2, tagId: TAG_TS },
312+
]);
313+
314+
// 'Go' matches nothing → none(Go) is satisfied by every user.
315+
const rows = await users
316+
.select('id', 'name')
317+
.where((u) => u.tags.none((t) => t.name.eq('Go')))
318+
.orderBy((u) => u.id.asc())
319+
.all();
320+
321+
expect(rows).toEqual([
322+
{ id: 1, name: 'Alice' },
323+
{ id: 2, name: 'Bob' },
324+
]);
325+
});
326+
},
327+
timeouts.spinUpPpgDev,
328+
);
329+
330+
it(
331+
'every with predicate that no tag satisfies excludes all tagged users (explicit select)',
332+
async () => {
333+
await withCollectionRuntime(async (runtime) => {
334+
const users = createUsersCollection(runtime);
335+
336+
await seedUsers(runtime, [
337+
{ id: 1, name: 'Alice', email: 'alice@example.com' },
338+
{ id: 2, name: 'Bob', email: 'bob@example.com' },
339+
]);
340+
await seedTags(runtime, [
341+
{ id: TAG_RUST, name: 'Rust' },
342+
{ id: TAG_TS, name: 'TypeScript' },
343+
]);
344+
await seedUserTags(runtime, [
345+
{ userId: 1, tagId: TAG_RUST },
346+
{ userId: 2, tagId: TAG_TS },
347+
]);
348+
349+
// every(name='Go') lowers to NOT EXISTS(… AND NOT(name='Go')).
350+
// Alice's Rust tag fails the predicate → NOT(pred) is true → EXISTS fires → excluded.
351+
// Bob's TypeScript tag fails the predicate → same → excluded.
352+
const rows = await users
353+
.select('id', 'name')
354+
.where((u) => u.tags.every((t) => t.name.eq('Go')))
355+
.orderBy((u) => u.id.asc())
356+
.all();
357+
358+
expect(rows).toEqual([]);
359+
});
360+
},
361+
timeouts.spinUpPpgDev,
362+
);
363+
});

0 commit comments

Comments
 (0)