Skip to content

Commit 31776a8

Browse files
ymc9claude
andauthored
fix(orm): use IS operator for null comparisons in filters (#2475)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent d3ab3a6 commit 31776a8

File tree

3 files changed

+91
-4
lines changed

3 files changed

+91
-4
lines changed

packages/orm/src/client/crud/dialects/base-dialect.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -757,11 +757,21 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
757757
}
758758

759759
protected buildJsonEqualityFilter(lhs: Expression<any>, rhs: unknown) {
760-
return this.buildLiteralFilter(lhs, 'Json', rhs);
760+
return this.buildValueFilter(lhs, 'Json', rhs);
761761
}
762762

763-
private buildLiteralFilter(lhs: Expression<any>, type: BuiltinType, rhs: unknown) {
764-
return this.eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformInput(rhs, type, false) : rhs);
763+
private buildValueFilter(lhs: Expression<any>, type: BuiltinType, rhs: unknown) {
764+
if (rhs === undefined) {
765+
// undefined filter is no-op, always true
766+
return this.true();
767+
}
768+
769+
if (rhs === null) {
770+
// null comparison
771+
return this.eb(lhs, 'is', null);
772+
}
773+
774+
return this.eb(lhs, '=', this.transformInput(rhs, type, false));
765775
}
766776

767777
private buildStandardFilter(
@@ -776,7 +786,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
776786
) {
777787
if (payload === null || !isPlainObject(payload)) {
778788
return {
779-
conditions: [this.buildLiteralFilter(lhs, type, payload)],
789+
conditions: [this.buildValueFilter(lhs, type, payload)],
780790
consumedKeys: [],
781791
};
782792
}

tests/e2e/orm/client-api/filter.test.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,6 +414,18 @@ describe('Client filter tests ', () => {
414414
where: { email: { not: { not: { contains: 'test' } } } },
415415
}),
416416
).toResolveTruthy();
417+
418+
// not null (issue #2472)
419+
await expect(
420+
client.user.findMany({
421+
where: { name: { not: null } },
422+
}),
423+
).toResolveWithLength(1);
424+
await expect(
425+
client.user.findFirst({
426+
where: { name: { not: null } },
427+
}),
428+
).resolves.toMatchObject({ id: user1.id });
417429
});
418430

419431
it('supports numeric filters', async () => {
@@ -490,6 +502,18 @@ describe('Client filter tests ', () => {
490502
where: { age: { not: { not: { equals: null } } } },
491503
}),
492504
).toResolveTruthy();
505+
506+
// not null shorthand (issue #2472)
507+
await expect(
508+
client.profile.findMany({
509+
where: { age: { not: null } },
510+
}),
511+
).toResolveWithLength(1);
512+
await expect(
513+
client.profile.findFirst({
514+
where: { age: { not: null } },
515+
}),
516+
).resolves.toMatchObject({ id: '1' });
493517
});
494518

495519
it('supports boolean filters', async () => {
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
// https://github.com/zenstackhq/zenstack/issues/2472
5+
// Filtering by `{ not: null }` returns empty array instead of non-null records
6+
describe('Regression for issue 2472', () => {
7+
const schema = `
8+
model Post {
9+
id Int @id @default(autoincrement())
10+
title String
11+
published_at DateTime?
12+
}
13+
`;
14+
15+
it('should filter records where nullable field is not null', async () => {
16+
const db = await createTestClient(schema);
17+
18+
await db.post.create({ data: { title: 'published', published_at: new Date('2025-01-01') } });
19+
await db.post.create({ data: { title: 'draft' } });
20+
21+
// { not: null } should return only records where the field is NOT NULL
22+
const results = await db.post.findMany({
23+
where: {
24+
published_at: {
25+
not: null,
26+
},
27+
},
28+
});
29+
30+
expect(results).toHaveLength(1);
31+
expect(results[0].title).toBe('published');
32+
});
33+
34+
it('should also work with { not: null } on string fields', async () => {
35+
const db = await createTestClient(`
36+
model Item {
37+
id Int @id @default(autoincrement())
38+
name String
39+
note String?
40+
}
41+
`);
42+
43+
await db.item.create({ data: { name: 'a', note: 'has note' } });
44+
await db.item.create({ data: { name: 'b' } });
45+
46+
const results = await db.item.findMany({
47+
where: { note: { not: null } },
48+
});
49+
50+
expect(results).toHaveLength(1);
51+
expect(results[0].name).toBe('a');
52+
});
53+
});

0 commit comments

Comments
 (0)