From f0330bc334fd3a8ed5377afcdd04b731baa8c753 Mon Sep 17 00:00:00 2001 From: Lars Lutz <56645452+larslutz96@users.noreply.github.com> Date: Wed, 15 Jan 2025 13:11:08 +0100 Subject: [PATCH] fix: starts endswith for null values (#975) Wrap `startswith` and `endswith` with `coalesce` to return false when element is `NULL`. --- db-service/lib/cql-functions.js | 4 ++-- postgres/lib/cql-functions.js | 4 ++-- test/scenarios/bookshop/funcs.test.js | 18 ++++++++++++++++++ 3 files changed, 22 insertions(+), 4 deletions(-) diff --git a/db-service/lib/cql-functions.js b/db-service/lib/cql-functions.js index 8c0d7ba19..90e3b64fc 100644 --- a/db-service/lib/cql-functions.js +++ b/db-service/lib/cql-functions.js @@ -75,7 +75,7 @@ const StandardFunctions = { * @param {string} y * @returns {string} */ - startswith: (x, y) => `instr(${x},${y}) = 1`, // sqlite instr is 1 indexed + startswith: (x, y) => `coalesce(instr(${x},${y}) = 1,false)`, // sqlite instr is 1 indexed // takes the end of the string of the size of the target and compares it with the target /** * Generates SQL statement that produces a boolean value indicating whether the first string ends with the second string @@ -83,7 +83,7 @@ const StandardFunctions = { * @param {string} y * @returns {string} */ - endswith: (x, y) => `substr(${x}, length(${x}) + 1 - length(${y})) = ${y}`, + endswith: (x, y) => `coalesce(substr(${x}, length(${x}) + 1 - length(${y})) = ${y},false)`, /** * Generates SQL statement that produces the substring of a given string * @example diff --git a/postgres/lib/cql-functions.js b/postgres/lib/cql-functions.js index 6c91c152b..009ba98e5 100644 --- a/postgres/lib/cql-functions.js +++ b/postgres/lib/cql-functions.js @@ -10,8 +10,8 @@ const StandardFunctions = { countdistinct: x => `count(distinct ${x.val || x || '*'})`, contains: (...args) => `(coalesce(strpos(${args}),0) > 0)`, indexof: (x, y) => `strpos(${x},${y}) - 1`, // strpos is 1 indexed - startswith: (x, y) => `strpos(${x},${y}) = 1`, // strpos is 1 indexed - endswith: (x, y) => `substr(${x},length(${x}) + 1 - length(${y})) = ${y}`, + startswith: (x, y) => `coalesce(strpos(${x},${y}) = 1,false)`, // strpos is 1 indexed + endswith: (x, y) => `coalesce(substr(${x},length(${x}) + 1 - length(${y})) = ${y},false)`, matchesPattern: (x, y) => `regexp_like(${x}, ${y})`, matchespattern: (x, y) => `regexp_like(${x}, ${y})`, diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index f3207fe05..18418136c 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -66,6 +66,15 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) + test('not endswith finds null', async () => { + const { Books } = cds.entities('sap.capire.bookshop') + await cds.run(INSERT({ ID: 123, title: 'Harry Potter', stock: undefined }).into(Books)) + const res = await GET(`/browse/Books?$filter=not endswith(author,'Poe')`) + expect(res.status).to.be.eq(200) + expect(res.data.value.some(item => item.ID === 123)).to.be.true + await cds.run(DELETE.from(Books).where({ ID: 123 })) + }) + test('indexof', async () => { const res = await GET(`/browse/Books?$filter=indexof(author,'Allen') eq 6`) @@ -91,6 +100,15 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) + test('not startswith finds null', async () => { + const { Books } = cds.entities('sap.capire.bookshop') + await cds.run(INSERT({ ID: 123, title: 'Harry Potter', stock: undefined }).into(Books)) + const res = await GET(`/browse/Books?$filter=not startswith(author,'Poe')`) + expect(res.status).to.be.eq(200) + expect(res.data.value.some(item => item.ID === 123)).to.be.true + await cds.run(DELETE.from(Books).where({ ID: 123 })) + }) + test('substring', async () => { const [three, two, negative] = await Promise.all([ GET(`/browse/Books?$filter=substring(author,1,2) eq 'dg'`),