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
140 changes: 140 additions & 0 deletions e2e/adapter/test/adapter-factory/basic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3174,6 +3174,146 @@ export const getNormalTestSuiteTests = (
expect(result!.email).toBe(user.email);
expect(result!.id).toBe(user.id);
},
"findMany - eq operator with null value (single condition) should use IS NULL":
async () => {
const withNull = await adapter.create<User>({
model: "user",
data: { ...(await generate("user")), image: null },
forceAllowId: true,
});
const withImage = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: "https://example.com/avatar.png",
},
forceAllowId: true,
});

const nullResult = await adapter.findMany<User>({
model: "user",
where: [{ field: "image", operator: "eq", value: null }],
});
const nullIds = nullResult.map((u) => u.id);
expect(nullIds).toContain(withNull.id);
expect(nullIds).not.toContain(withImage.id);

const notNullResult = await adapter.findMany<User>({
model: "user",
where: [{ field: "image", operator: "ne", value: null }],
});
const notNullIds = notNullResult.map((u) => u.id);
expect(notNullIds).not.toContain(withNull.id);
expect(notNullIds).toContain(withImage.id);
},

"findMany - eq and ne operators with null value in AND group should use IS NULL / IS NOT NULL":
async () => {
const nullVerified = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: null,
emailVerified: true,
},
forceAllowId: true,
});
const nullUnverified = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: null,
emailVerified: false,
},
forceAllowId: true,
});
const imageVerified = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: "https://example.com/avatar.png",
emailVerified: true,
},
forceAllowId: true,
});

// image IS NULL AND emailVerified = true → only nullVerified
const eqResult = await adapter.findMany<User>({
model: "user",
where: [
{ field: "image", operator: "eq", value: null, connector: "AND" },
{ field: "emailVerified", value: true, connector: "AND" },
],
});
const eqIds = eqResult.map((u) => u.id);
expect(eqIds).toContain(nullVerified.id);
expect(eqIds).not.toContain(nullUnverified.id);
expect(eqIds).not.toContain(imageVerified.id);

// image IS NOT NULL AND emailVerified = true → only imageVerified
const neResult = await adapter.findMany<User>({
model: "user",
where: [
{ field: "image", operator: "ne", value: null, connector: "AND" },
{ field: "emailVerified", value: true, connector: "AND" },
],
});
const neIds = neResult.map((u) => u.id);
expect(neIds).not.toContain(nullVerified.id);
expect(neIds).not.toContain(nullUnverified.id);
expect(neIds).toContain(imageVerified.id);
},

"findMany - eq and ne operators with null value in OR group should use IS NULL / IS NOT NULL":
async () => {
const withNull = await adapter.create<User>({
model: "user",
data: { ...(await generate("user")), image: null },
forceAllowId: true,
});
const targetImage = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: "https://example.com/target.png",
},
forceAllowId: true,
});
const otherImage = await adapter.create<User>({
model: "user",
data: {
...(await generate("user")),
image: "https://example.com/other.png",
},
forceAllowId: true,
});

// image IS NULL OR email = targetImage.email → withNull + targetImage
const eqResult = await adapter.findMany<User>({
model: "user",
where: [
{ field: "image", operator: "eq", value: null, connector: "OR" },
{ field: "email", value: targetImage.email, connector: "OR" },
],
});
const eqIds = eqResult.map((u) => u.id);
expect(eqIds).toContain(withNull.id);
expect(eqIds).toContain(targetImage.id);
expect(eqIds).not.toContain(otherImage.id);

// image IS NOT NULL OR email = withNull.email → targetImage + otherImage + withNull (by email)
const neResult = await adapter.findMany<User>({
model: "user",
where: [
{ field: "image", operator: "ne", value: null, connector: "OR" },
{ field: "email", value: withNull.email, connector: "OR" },
],
});
const neIds = neResult.map((u) => u.id);
expect(neIds).toContain(withNull.id); // matched by email OR clause
expect(neIds).toContain(targetImage.id);
expect(neIds).toContain(otherImage.id);
},
};
};

Expand Down
2 changes: 1 addition & 1 deletion packages/btst/adapter-drizzle/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/adapter-drizzle",
"version": "2.1.0",
"version": "2.1.1",
"description": "Drizzle adapter for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/adapter-kysely/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/adapter-kysely",
"version": "2.1.0",
"version": "2.1.1",
"description": "Kysely adapter for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/adapter-memory/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/adapter-memory",
"version": "2.1.0",
"version": "2.1.1",
"description": "In-memory adapter for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/adapter-mongodb/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/adapter-mongodb",
"version": "2.1.0",
"version": "2.1.1",
"description": "MongoDB adapter for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/adapter-prisma/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/adapter-prisma",
"version": "2.1.0",
"version": "2.1.1",
"description": "Prisma adapter for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/cli/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/cli",
"version": "2.1.0",
"version": "2.1.1",
"description": "CLI for btst schema generation and migration",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/db/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/db",
"version": "2.1.0",
"version": "2.1.1",
"description": "Core database utilities and schema definition for btst",
"type": "module",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion packages/btst/plugins/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@btst/plugins",
"version": "2.1.0",
"version": "2.1.1",
"description": "Plugin utilities and common plugins for btst",
"type": "module",
"license": "MIT",
Expand Down
30 changes: 24 additions & 6 deletions packages/drizzle-adapter/src/drizzle-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import {
gt,
gte,
inArray,
isNotNull,
isNull,
like,
lt,
lte,
Expand Down Expand Up @@ -215,7 +217,11 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
}

if (w.operator === "ne") {
return [ne(schemaModel[field], w.value)];
return [
w.value === null
? isNotNull(schemaModel[field])
: ne(schemaModel[field], w.value),
];
}

if (w.operator === "gt") {
Expand All @@ -226,7 +232,11 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
return [gte(schemaModel[field], w.value)];
}

return [eq(schemaModel[field], w.value)];
return [
w.value === null
? isNull(schemaModel[field])
: eq(schemaModel[field], w.value),
];
}
const andGroup = where.filter(
(w) => w.connector === "AND" || !w.connector,
Expand Down Expand Up @@ -274,9 +284,13 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
return gte(schemaModel[field], w.value);
}
if (w.operator === "ne") {
return ne(schemaModel[field], w.value);
return w.value === null
? isNotNull(schemaModel[field])
: ne(schemaModel[field], w.value);
}
return eq(schemaModel[field], w.value);
return w.value === null
? isNull(schemaModel[field])
: eq(schemaModel[field], w.value);
}),
);
const orClause = or(
Expand Down Expand Up @@ -320,9 +334,13 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => {
return gte(schemaModel[field], w.value);
}
if (w.operator === "ne") {
return ne(schemaModel[field], w.value);
return w.value === null
? isNotNull(schemaModel[field])
: ne(schemaModel[field], w.value);
}
return eq(schemaModel[field], w.value);
return w.value === null
? isNull(schemaModel[field])
: eq(schemaModel[field], w.value);
}),
);

Expand Down
6 changes: 4 additions & 2 deletions packages/kysely-adapter/src/kysely-adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -187,11 +187,13 @@ export const kyselyAdapter = (
}

if (operator === "eq") {
return eb(f, "=", value);
return value === null ? eb(f, "is", null) : eb(f, "=", value);
}

if (operator === "ne") {
return eb(f, "<>", value);
return value === null
? eb(f, "is not", null)
: eb(f, "<>", value);
}

if (operator === "gt") {
Expand Down
Loading