From 189d623ac9e3ffe151853931d53302f5f418232a Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Tue, 17 Mar 2026 17:01:32 -0400 Subject: [PATCH 1/5] fix: adapter equality bug --- packages/btst/adapter-drizzle/package.json | 2 +- packages/btst/adapter-kysely/package.json | 2 +- packages/btst/adapter-memory/package.json | 2 +- packages/btst/adapter-mongodb/package.json | 2 +- packages/btst/adapter-prisma/package.json | 2 +- packages/btst/cli/package.json | 2 +- packages/btst/db/package.json | 2 +- packages/btst/plugins/package.json | 2 +- .../drizzle-adapter/src/drizzle-adapter.ts | 35 ++++++++++--------- packages/kysely-adapter/src/kysely-adapter.ts | 12 +++---- 10 files changed, 32 insertions(+), 31 deletions(-) diff --git a/packages/btst/adapter-drizzle/package.json b/packages/btst/adapter-drizzle/package.json index 3f584fb64b6..fef7e92d662 100644 --- a/packages/btst/adapter-drizzle/package.json +++ b/packages/btst/adapter-drizzle/package.json @@ -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", diff --git a/packages/btst/adapter-kysely/package.json b/packages/btst/adapter-kysely/package.json index 15307f57aef..b81ec4cbb3e 100644 --- a/packages/btst/adapter-kysely/package.json +++ b/packages/btst/adapter-kysely/package.json @@ -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", diff --git a/packages/btst/adapter-memory/package.json b/packages/btst/adapter-memory/package.json index 38a8dc6a379..32cf2f5a9c5 100644 --- a/packages/btst/adapter-memory/package.json +++ b/packages/btst/adapter-memory/package.json @@ -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", diff --git a/packages/btst/adapter-mongodb/package.json b/packages/btst/adapter-mongodb/package.json index 733224672ca..923615bd6f2 100644 --- a/packages/btst/adapter-mongodb/package.json +++ b/packages/btst/adapter-mongodb/package.json @@ -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", diff --git a/packages/btst/adapter-prisma/package.json b/packages/btst/adapter-prisma/package.json index 8bc6bb4d94f..1340f32f862 100644 --- a/packages/btst/adapter-prisma/package.json +++ b/packages/btst/adapter-prisma/package.json @@ -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", diff --git a/packages/btst/cli/package.json b/packages/btst/cli/package.json index 278504c5064..a7307b6a2a2 100644 --- a/packages/btst/cli/package.json +++ b/packages/btst/cli/package.json @@ -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", diff --git a/packages/btst/db/package.json b/packages/btst/db/package.json index cc576621f59..70309762dfb 100644 --- a/packages/btst/db/package.json +++ b/packages/btst/db/package.json @@ -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", diff --git a/packages/btst/plugins/package.json b/packages/btst/plugins/package.json index ddcd3e903ae..145c060e136 100644 --- a/packages/btst/plugins/package.json +++ b/packages/btst/plugins/package.json @@ -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", diff --git a/packages/drizzle-adapter/src/drizzle-adapter.ts b/packages/drizzle-adapter/src/drizzle-adapter.ts index b92167a1e00..69d3250dc53 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.ts @@ -19,6 +19,7 @@ import { gt, gte, inArray, + isNull, like, lt, lte, @@ -226,9 +227,9 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { return [gte(schemaModel[field], w.value)]; } - return [eq(schemaModel[field], w.value)]; - } - const andGroup = where.filter( + return [w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value)]; + } + const andGroup = where.filter( (w) => w.connector === "AND" || !w.connector, ); const orGroup = where.filter((w) => w.connector === "OR"); @@ -273,13 +274,13 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return eq(schemaModel[field], w.value); - }), - ); - const orClause = or( + if (w.operator === "ne") { + return ne(schemaModel[field], w.value); + } + return w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value); + }), + ); + const orClause = or( ...orGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { @@ -319,14 +320,14 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return eq(schemaModel[field], w.value); - }), - ); + if (w.operator === "ne") { + return ne(schemaModel[field], w.value); + } + return w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value); + }), + ); - const clause: SQL[] = []; + const clause: SQL[] = []; if (andGroup.length) clause.push(andClause!); if (orGroup.length) clause.push(orClause!); diff --git a/packages/kysely-adapter/src/kysely-adapter.ts b/packages/kysely-adapter/src/kysely-adapter.ts index 7934ddec619..a4493d88a8d 100644 --- a/packages/kysely-adapter/src/kysely-adapter.ts +++ b/packages/kysely-adapter/src/kysely-adapter.ts @@ -186,13 +186,13 @@ export const kyselyAdapter = ( return eb(f, "like", `%${value}`); } - if (operator === "eq") { - return eb(f, "=", value); - } + if (operator === "eq") { + return value === null ? eb(f, "is", null) : eb(f, "=", value); + } - if (operator === "ne") { - return eb(f, "<>", value); - } + if (operator === "ne") { + return value === null ? eb(f, "is not", null) : eb(f, "<>", value); + } if (operator === "gt") { return eb(f, ">", value); From 17e1f598dc188cb91480b9abc6f384be69124dd7 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Tue, 17 Mar 2026 17:06:14 -0400 Subject: [PATCH 2/5] lint: fix issues --- .../drizzle-adapter/src/drizzle-adapter.ts | 42 +++++++++++-------- packages/kysely-adapter/src/kysely-adapter.ts | 14 ++++--- 2 files changed, 33 insertions(+), 23 deletions(-) diff --git a/packages/drizzle-adapter/src/drizzle-adapter.ts b/packages/drizzle-adapter/src/drizzle-adapter.ts index 69d3250dc53..6a1730eddde 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.ts @@ -227,9 +227,13 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { return [gte(schemaModel[field], w.value)]; } - return [w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value)]; - } - const andGroup = where.filter( + return [ + w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value), + ]; + } + const andGroup = where.filter( (w) => w.connector === "AND" || !w.connector, ); const orGroup = where.filter((w) => w.connector === "OR"); @@ -274,13 +278,15 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value); - }), - ); - const orClause = or( + if (w.operator === "ne") { + return ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); + const orClause = or( ...orGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { @@ -320,14 +326,16 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return w.value === null ? isNull(schemaModel[field]) : eq(schemaModel[field], w.value); - }), - ); + if (w.operator === "ne") { + return ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); - const clause: SQL[] = []; + const clause: SQL[] = []; if (andGroup.length) clause.push(andClause!); if (orGroup.length) clause.push(orClause!); diff --git a/packages/kysely-adapter/src/kysely-adapter.ts b/packages/kysely-adapter/src/kysely-adapter.ts index a4493d88a8d..1e46b29ce78 100644 --- a/packages/kysely-adapter/src/kysely-adapter.ts +++ b/packages/kysely-adapter/src/kysely-adapter.ts @@ -186,13 +186,15 @@ export const kyselyAdapter = ( return eb(f, "like", `%${value}`); } - if (operator === "eq") { - return value === null ? eb(f, "is", null) : eb(f, "=", value); - } + if (operator === "eq") { + return value === null ? eb(f, "is", null) : eb(f, "=", value); + } - if (operator === "ne") { - return value === null ? eb(f, "is not", null) : eb(f, "<>", value); - } + if (operator === "ne") { + return value === null + ? eb(f, "is not", null) + : eb(f, "<>", value); + } if (operator === "gt") { return eb(f, ">", value); From 4facad30412905b1436da32ebffc358ed968338e Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Tue, 17 Mar 2026 17:11:52 -0400 Subject: [PATCH 3/5] fix(adapter): handle null values in inequality checks using isNotNull --- .../drizzle-adapter/src/drizzle-adapter.ts | 39 ++++++++++--------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/drizzle-adapter/src/drizzle-adapter.ts b/packages/drizzle-adapter/src/drizzle-adapter.ts index 6a1730eddde..67a350a8ca8 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.ts @@ -19,6 +19,7 @@ import { gt, gte, inArray, + isNotNull, isNull, like, lt, @@ -216,7 +217,7 @@ 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") { @@ -278,15 +279,15 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return w.value === null - ? isNull(schemaModel[field]) - : eq(schemaModel[field], w.value); - }), - ); - const orClause = or( + if (w.operator === "ne") { + return w.value === null ? isNotNull(schemaModel[field]) : ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); + const orClause = or( ...orGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { @@ -326,16 +327,16 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return ne(schemaModel[field], w.value); - } - return w.value === null - ? isNull(schemaModel[field]) - : eq(schemaModel[field], w.value); - }), - ); + if (w.operator === "ne") { + return w.value === null ? isNotNull(schemaModel[field]) : ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); - const clause: SQL[] = []; + const clause: SQL[] = []; if (andGroup.length) clause.push(andClause!); if (orGroup.length) clause.push(orClause!); From 5ab156388e05fe71c8a386ac2d96206afbc531ef Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Tue, 17 Mar 2026 17:16:29 -0400 Subject: [PATCH 4/5] lint: fix --- .../drizzle-adapter/src/drizzle-adapter.ts | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/packages/drizzle-adapter/src/drizzle-adapter.ts b/packages/drizzle-adapter/src/drizzle-adapter.ts index 67a350a8ca8..491d9f2d986 100644 --- a/packages/drizzle-adapter/src/drizzle-adapter.ts +++ b/packages/drizzle-adapter/src/drizzle-adapter.ts @@ -217,7 +217,11 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { } if (w.operator === "ne") { - return [w.value === null ? isNotNull(schemaModel[field]) : ne(schemaModel[field], w.value)]; + return [ + w.value === null + ? isNotNull(schemaModel[field]) + : ne(schemaModel[field], w.value), + ]; } if (w.operator === "gt") { @@ -279,15 +283,17 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return w.value === null ? isNotNull(schemaModel[field]) : ne(schemaModel[field], w.value); - } - return w.value === null - ? isNull(schemaModel[field]) - : eq(schemaModel[field], w.value); - }), - ); - const orClause = or( + if (w.operator === "ne") { + return w.value === null + ? isNotNull(schemaModel[field]) + : ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); + const orClause = or( ...orGroup.map((w) => { const field = getFieldName({ model, field: w.field }); if (w.operator === "in") { @@ -327,16 +333,18 @@ export const drizzleAdapter = (db: DB, config: DrizzleAdapterConfig) => { if (w.operator === "gte") { return gte(schemaModel[field], w.value); } - if (w.operator === "ne") { - return w.value === null ? isNotNull(schemaModel[field]) : ne(schemaModel[field], w.value); - } - return w.value === null - ? isNull(schemaModel[field]) - : eq(schemaModel[field], w.value); - }), - ); + if (w.operator === "ne") { + return w.value === null + ? isNotNull(schemaModel[field]) + : ne(schemaModel[field], w.value); + } + return w.value === null + ? isNull(schemaModel[field]) + : eq(schemaModel[field], w.value); + }), + ); - const clause: SQL[] = []; + const clause: SQL[] = []; if (andGroup.length) clause.push(andClause!); if (orGroup.length) clause.push(orClause!); From d687b074b2c9fe74489b847ebb5fe4d714e44e03 Mon Sep 17 00:00:00 2001 From: olliethedev <3martynov@gmail.com> Date: Tue, 17 Mar 2026 17:36:03 -0400 Subject: [PATCH 5/5] test(adapter): add tests for handling null values with eq and ne operators in findMany queries --- e2e/adapter/test/adapter-factory/basic.ts | 140 ++++++++++++++++++++++ 1 file changed, 140 insertions(+) diff --git a/e2e/adapter/test/adapter-factory/basic.ts b/e2e/adapter/test/adapter-factory/basic.ts index 1654a2550a5..7fcce563789 100644 --- a/e2e/adapter/test/adapter-factory/basic.ts +++ b/e2e/adapter/test/adapter-factory/basic.ts @@ -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({ + model: "user", + data: { ...(await generate("user")), image: null }, + forceAllowId: true, + }); + const withImage = await adapter.create({ + model: "user", + data: { + ...(await generate("user")), + image: "https://example.com/avatar.png", + }, + forceAllowId: true, + }); + + const nullResult = await adapter.findMany({ + 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({ + 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({ + model: "user", + data: { + ...(await generate("user")), + image: null, + emailVerified: true, + }, + forceAllowId: true, + }); + const nullUnverified = await adapter.create({ + model: "user", + data: { + ...(await generate("user")), + image: null, + emailVerified: false, + }, + forceAllowId: true, + }); + const imageVerified = await adapter.create({ + 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({ + 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({ + 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({ + model: "user", + data: { ...(await generate("user")), image: null }, + forceAllowId: true, + }); + const targetImage = await adapter.create({ + model: "user", + data: { + ...(await generate("user")), + image: "https://example.com/target.png", + }, + forceAllowId: true, + }); + const otherImage = await adapter.create({ + 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({ + 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({ + 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); + }, }; };