Skip to content

Commit 3d96eec

Browse files
Merge pull request #3 from hcengineering/fix-ne-predicate
Fix $ne predicate query
2 parents 6aa0db0 + 05419bd commit 3d96eec

File tree

4 files changed

+149
-3
lines changed

4 files changed

+149
-3
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
{
2+
"changes": [
3+
{
4+
"packageName": "@hcengineering/postgres",
5+
"comment": "Fix ne predicate",
6+
"type": "patch"
7+
}
8+
],
9+
"packageName": "@hcengineering/postgres"
10+
}

packages/postgres/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@hcengineering/postgres",
3-
"version": "0.7.21",
3+
"version": "0.7.22",
44
"main": "lib/index.js",
55
"svelte": "src/index.ts",
66
"types": "types/index.d.ts",

packages/postgres/src/__tests__/integration.test.ts

Lines changed: 137 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
type PostgresClientReference
4242
} from '..'
4343
import { genMinModel } from './minmodel'
44-
import { createTaskModel, type Task, type TaskComment, taskPlugin } from './tasks'
44+
import { createTaskModel, TaskReproduce, TaskStatus, type Task, type TaskComment, taskPlugin } from './tasks'
4545

4646
const txes = genMinModel()
4747
createTaskModel(txes)
@@ -923,6 +923,142 @@ describe('PostgreSQL Integration Tests (Real Database)', () => {
923923
})
924924
})
925925

926+
describe('$ne predicate with missing fields', () => {
927+
let taskWithRate100: Ref<Task>
928+
let taskWithRate50: Ref<Task>
929+
let taskWithRateNull: Ref<Task>
930+
let taskWithoutRate: Ref<Task>
931+
932+
beforeEach(async () => {
933+
// Create tasks with different rate scenarios
934+
taskWithRate100 = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
935+
name: 'Task with rate 100',
936+
description: 'Should not match $ne: 100',
937+
rate: 100
938+
})
939+
940+
taskWithRate50 = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
941+
name: 'Task with rate 50',
942+
description: 'Should match $ne: 100',
943+
rate: 50
944+
})
945+
946+
taskWithRateNull = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
947+
name: 'Task with rate null',
948+
description: 'Should match $ne: 100 (null != 100)',
949+
rate: null
950+
})
951+
952+
// Create task without rate field (missing field)
953+
taskWithoutRate = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
954+
name: 'Task without rate field',
955+
description: 'Should match $ne: 100 (missing field should match)'
956+
// rate field is not provided - it will be missing/undefined in the document
957+
})
958+
})
959+
960+
it('should match documents with missing fields when using $ne', async () => {
961+
const tasks = await client.findAll<Task>(taskPlugin.class.Task, { rate: { $ne: 100 } })
962+
963+
// Should match:
964+
// - Task with rate 50 (different value)
965+
// - Task with rate null (null != 100)
966+
// - Task without rate field (missing field should match)
967+
// Should NOT match:
968+
// - Task with rate 100
969+
970+
expect(tasks).toHaveLength(3)
971+
972+
const taskIds = tasks.map((t) => t._id)
973+
expect(taskIds).not.toContain(taskWithRate100)
974+
expect(taskIds).toContain(taskWithRate50)
975+
expect(taskIds).toContain(taskWithRateNull)
976+
expect(taskIds).toContain(taskWithoutRate)
977+
978+
// Verify that the task without rate field has undefined or null rate
979+
const taskWithoutRateDoc = tasks.find((t) => t._id === taskWithoutRate)
980+
expect(taskWithoutRateDoc).toBeDefined()
981+
expect(taskWithoutRateDoc?.rate === undefined || taskWithoutRateDoc?.rate === null).toBe(true)
982+
})
983+
984+
it('should match documents with missing fields when using $ne with boolean true', async () => {
985+
// Test with a boolean field scenario
986+
// Create tasks with status field (which is optional)
987+
const taskWithStatusOpen = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
988+
name: 'Task with status Open',
989+
description: 'Has status',
990+
status: TaskStatus.Open
991+
})
992+
993+
const taskWithStatusClose = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
994+
name: 'Task with status Close',
995+
description: 'Has different status',
996+
status: TaskStatus.Close
997+
})
998+
999+
const taskWithoutStatus = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
1000+
name: 'Task without status',
1001+
description: 'Missing status field'
1002+
// status field is not provided
1003+
})
1004+
1005+
// Query for tasks where status is not Open
1006+
const tasks = await client.findAll<Task>(taskPlugin.class.Task, { status: { $ne: TaskStatus.Open } })
1007+
1008+
// Should match:
1009+
// - Task with status Close (different value)
1010+
// - Task without status field (missing field should match)
1011+
// Should NOT match:
1012+
// - Task with status Open
1013+
1014+
expect(tasks.length).toBeGreaterThanOrEqual(2)
1015+
1016+
const taskIds = tasks.map((t) => t._id)
1017+
expect(taskIds).not.toContain(taskWithStatusOpen)
1018+
expect(taskIds).toContain(taskWithStatusClose)
1019+
expect(taskIds).toContain(taskWithoutStatus)
1020+
})
1021+
1022+
it('should handle $ne with string value and missing fields', async () => {
1023+
// Test with a string field that can be missing
1024+
const taskWithReproduceAlways = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
1025+
name: 'Task with reproduce Always',
1026+
description: 'Has reproduce field',
1027+
reproduce: TaskReproduce.Always
1028+
})
1029+
1030+
const taskWithReproduceRare = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
1031+
name: 'Task with reproduce Rare',
1032+
description: 'Has different reproduce value',
1033+
reproduce: TaskReproduce.Rare
1034+
})
1035+
1036+
const taskWithoutReproduce = await operations.createDoc(taskPlugin.class.Task, '' as Ref<Space>, {
1037+
name: 'Task without reproduce field',
1038+
description: 'Missing reproduce field'
1039+
// reproduce field is not provided
1040+
})
1041+
1042+
// Query for tasks where reproduce is not Always
1043+
const tasks = await client.findAll<Task>(taskPlugin.class.Task, {
1044+
reproduce: { $ne: TaskReproduce.Always }
1045+
})
1046+
1047+
// Should match:
1048+
// - Task with reproduce Rare (different value)
1049+
// - Task without reproduce field (missing field should match)
1050+
// Should NOT match:
1051+
// - Task with reproduce Always
1052+
1053+
expect(tasks.length).toBeGreaterThanOrEqual(2)
1054+
1055+
const taskIds = tasks.map((t) => t._id)
1056+
expect(taskIds).not.toContain(taskWithReproduceAlways)
1057+
expect(taskIds).toContain(taskWithReproduceRare)
1058+
expect(taskIds).toContain(taskWithoutReproduce)
1059+
})
1060+
})
1061+
9261062
describe('Projection with Lookups Combined', () => {
9271063
let taskId: Ref<Task>
9281064

packages/postgres/src/storage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1255,7 +1255,7 @@ abstract class PostgresAdapterBase implements DbAdapter {
12551255
if (val == null) {
12561256
res.push(`${tlkey} IS NOT NULL`)
12571257
} else {
1258-
res.push(`${tlkey} != ${vars.add(val, valType)}`)
1258+
res.push(`(${tlkey} != ${vars.add(val, valType)} OR ${tkey} IS NULL)`)
12591259
}
12601260
break
12611261
case '$gt':

0 commit comments

Comments
 (0)