From c0f9073f35f8cfc3bf29810613ea889203672f0d Mon Sep 17 00:00:00 2001 From: joel-rich <6070705+joel-rich@users.noreply.github.com> Date: Tue, 5 Nov 2024 01:39:43 -0700 Subject: [PATCH] Fixes #3682 - Fix $ne filters incorrectly excluding null values (#3686) * allow matching null values when not equals query filter is set * Also fix namedParameter queries with filter * improve tests * release note --------- Co-authored-by: Joel Rich --- packages/loot-core/src/server/aql/compiler.ts | 6 +-- .../loot-core/src/server/aql/exec.test.ts | 44 +++++++++++++++++++ upcoming-release-notes/3686.md | 6 +++ 3 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 upcoming-release-notes/3686.md diff --git a/packages/loot-core/src/server/aql/compiler.ts b/packages/loot-core/src/server/aql/compiler.ts index b5c1d4a7a25..de665f9e0b3 100644 --- a/packages/loot-core/src/server/aql/compiler.ts +++ b/packages/loot-core/src/server/aql/compiler.ts @@ -706,12 +706,12 @@ const compileOp = saveStack('op', (state, fieldRef, opData) => { state.namedParameters = [].concat.apply([], orders); return `CASE - WHEN ${left} IS NULL THEN ${right} IS NULL - ELSE ${left} != ${right} + WHEN ${left} IS NULL THEN ${right} IS NOT NULL + ELSE ${left} IS NOT ${right} END`; } - return `${left} != ${right}`; + return `(${left} != ${right} OR ${left} IS NULL)`; } case '$oneof': { const [left, right] = valArray(state, [lhs, rhs], [null, 'array']); diff --git a/packages/loot-core/src/server/aql/exec.test.ts b/packages/loot-core/src/server/aql/exec.test.ts index 36b08bc457c..0a7bdf48cd2 100644 --- a/packages/loot-core/src/server/aql/exec.test.ts +++ b/packages/loot-core/src/server/aql/exec.test.ts @@ -167,6 +167,7 @@ describe('runQuery', () => { it('allows null as a parameter', async () => { await db.insertCategoryGroup({ id: 'group', name: 'group' }); await db.insertCategory({ id: 'cat', name: 'cat', cat_group: 'group' }); + await db.insertCategory({ id: 'cat2', name: 'cat2', cat_group: 'group' }); const transNoCat = await db.insertTransaction({ account: 'acct', date: '2020-01-01', @@ -179,6 +180,12 @@ describe('runQuery', () => { amount: -5001, category: 'cat', }); + const transCat2 = await db.insertTransaction({ + account: 'acct', + date: '2020-01-02', + amount: -5001, + category: 'cat2', + }); const queryState = q('transactions') .filter({ category: ':category' }) @@ -190,6 +197,43 @@ describe('runQuery', () => { data = (await runQuery(queryState, { params: { category: 'cat' } })).data; expect(data[0].id).toBe(transCat); + + data = ( + await runQuery( + q('transactions') + .filter({ category: { $ne: ':category' } }) + .select('category') + .serialize(), + + { params: { category: 'cat2' } }, + ) + ).data; + expect(data).toHaveLength(2); + expect(data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ id: transNoCat }), + expect.objectContaining({ id: transCat }), + expect.not.objectContaining({ id: transCat2 }), + ]), + ); + + data = ( + await runQuery( + q('transactions') + .filter({ category: { $ne: ':category' } }) + .select('category') + .serialize(), + { params: { category: null } }, + ) + ).data; + expect(data).toHaveLength(2); + expect(data).toEqual( + expect.arrayContaining([ + expect.not.objectContaining({ id: transNoCat }), + expect.objectContaining({ id: transCat }), + expect.objectContaining({ id: transCat2 }), + ]), + ); }); it('parameters have the correct order', async () => { diff --git a/upcoming-release-notes/3686.md b/upcoming-release-notes/3686.md new file mode 100644 index 00000000000..63858b96836 --- /dev/null +++ b/upcoming-release-notes/3686.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [joel-rich] +--- + +Fixes #3682 - Fix $ne filters incorrectly excluding null values