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
18 changes: 14 additions & 4 deletions packages/orm/src/client/crud/dialects/base-dialect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -757,11 +757,21 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
}

protected buildJsonEqualityFilter(lhs: Expression<any>, rhs: unknown) {
return this.buildLiteralFilter(lhs, 'Json', rhs);
return this.buildValueFilter(lhs, 'Json', rhs);
}

private buildLiteralFilter(lhs: Expression<any>, type: BuiltinType, rhs: unknown) {
return this.eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformInput(rhs, type, false) : rhs);
private buildValueFilter(lhs: Expression<any>, type: BuiltinType, rhs: unknown) {
if (rhs === undefined) {
// undefined filter is no-op, always true
return this.true();
}

if (rhs === null) {
// null comparison
return this.eb(lhs, 'is', null);
}

return this.eb(lhs, '=', this.transformInput(rhs, type, false));
}

private buildStandardFilter(
Expand All @@ -776,7 +786,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
) {
if (payload === null || !isPlainObject(payload)) {
return {
conditions: [this.buildLiteralFilter(lhs, type, payload)],
conditions: [this.buildValueFilter(lhs, type, payload)],
consumedKeys: [],
};
}
Expand Down
24 changes: 24 additions & 0 deletions tests/e2e/orm/client-api/filter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,18 @@ describe('Client filter tests ', () => {
where: { email: { not: { not: { contains: 'test' } } } },
}),
).toResolveTruthy();

// not null (issue #2472)
await expect(
client.user.findMany({
where: { name: { not: null } },
}),
).toResolveWithLength(1);
await expect(
client.user.findFirst({
where: { name: { not: null } },
}),
).resolves.toMatchObject({ id: user1.id });
});

it('supports numeric filters', async () => {
Expand Down Expand Up @@ -490,6 +502,18 @@ describe('Client filter tests ', () => {
where: { age: { not: { not: { equals: null } } } },
}),
).toResolveTruthy();

// not null shorthand (issue #2472)
await expect(
client.profile.findMany({
where: { age: { not: null } },
}),
).toResolveWithLength(1);
await expect(
client.profile.findFirst({
where: { age: { not: null } },
}),
).resolves.toMatchObject({ id: '1' });
});

it('supports boolean filters', async () => {
Expand Down
53 changes: 53 additions & 0 deletions tests/regression/test/issue-2472.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createTestClient } from '@zenstackhq/testtools';
import { describe, expect, it } from 'vitest';

// https://github.com/zenstackhq/zenstack/issues/2472
// Filtering by `{ not: null }` returns empty array instead of non-null records
describe('Regression for issue 2472', () => {
const schema = `
model Post {
id Int @id @default(autoincrement())
title String
published_at DateTime?
}
`;

it('should filter records where nullable field is not null', async () => {
const db = await createTestClient(schema);

await db.post.create({ data: { title: 'published', published_at: new Date('2025-01-01') } });
await db.post.create({ data: { title: 'draft' } });

// { not: null } should return only records where the field is NOT NULL
const results = await db.post.findMany({
where: {
published_at: {
not: null,
},
},
});

expect(results).toHaveLength(1);
expect(results[0].title).toBe('published');
});

it('should also work with { not: null } on string fields', async () => {
const db = await createTestClient(`
model Item {
id Int @id @default(autoincrement())
name String
note String?
}
`);

await db.item.create({ data: { name: 'a', note: 'has note' } });
await db.item.create({ data: { name: 'b' } });

const results = await db.item.findMany({
where: { note: { not: null } },
});

expect(results).toHaveLength(1);
expect(results[0].name).toBe('a');
});
});
Loading