From 5fbfd8d8cbe326f79d6cffe3b6f91de40642068b Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Thu, 9 Jan 2025 15:06:31 +0100 Subject: [PATCH 1/7] Update cqn2sql.js --- db-service/lib/cqn2sql.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 72cd82d1a..d8d1514e7 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -875,7 +875,13 @@ class CQN2SQLRenderer { : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_ - + + // Wrap not with functions with coalesce to always return true or false + if (x === 'not' && typeof xpr[i + 1] !== 'string' && 'func' in xpr[i + 1]) { + xpr[i + 1] = { args: [xpr[i + 1], { val: false, param: false }], func: 'coalesce' } + return x + } + else return x function _inline_null(n) { From 9b8d04a27996703497b48185435f8843733f5db5 Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:53:08 +0100 Subject: [PATCH 2/7] only use coalesce for starts and endswith --- db-service/lib/cql-functions.js | 4 ++-- db-service/lib/cqn2sql.js | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/db-service/lib/cql-functions.js b/db-service/lib/cql-functions.js index 2f1f8f7f0..df0630978 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/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index d8d1514e7..8975ecf68 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -876,12 +876,6 @@ class CQN2SQLRenderer { ? '<>' : this.is_distinct_from_ - // Wrap not with functions with coalesce to always return true or false - if (x === 'not' && typeof xpr[i + 1] !== 'string' && 'func' in xpr[i + 1]) { - xpr[i + 1] = { args: [xpr[i + 1], { val: false, param: false }], func: 'coalesce' } - return x - } - else return x function _inline_null(n) { From 424a240305acfefb96f0449ab67c78f3d2141f67 Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 10:53:33 +0100 Subject: [PATCH 3/7] Update cqn2sql.js --- db-service/lib/cqn2sql.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 8975ecf68..72cd82d1a 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -875,7 +875,7 @@ class CQN2SQLRenderer { : _not_null(i - 1) && _not_null(i + 1) ? '<>' : this.is_distinct_from_ - + else return x function _inline_null(n) { From bac3346b38a60fe25efe50f73a9bd0b025d22b40 Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:10:05 +0100 Subject: [PATCH 4/7] tests --- test/scenarios/bookshop/funcs.test.js | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index b634f101b..88de79a53 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -42,6 +42,15 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) + test('not endswith', async () => { + const { Books } = cds.entities('sap.capire.bookshop') + await cds.run(INSERT.into(Books).columns(['ID']).rows([123])) + const res = await GET(`/browse/Books?$filter=not endswith(title,'Sturm')`) + 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`) @@ -67,6 +76,15 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) + test('not startswith', async () => { + const { Books } = cds.entities('sap.capire.bookshop') + await cds.run(INSERT.into(Books).columns(['ID']).rows([123])) + const res = await GET(`/browse/Books?$filter=not startswith(title,'Sturm')`) + 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'`), From 504bcb220a4266bffb182b54175b0b30915701b4 Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 11:49:22 +0100 Subject: [PATCH 5/7] Update funcs.test.js --- test/scenarios/bookshop/funcs.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index 88de79a53..ef177bdc0 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -42,13 +42,13 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) - test('not endswith', async () => { + test('not endswith findes null', async () => { const { Books } = cds.entities('sap.capire.bookshop') - await cds.run(INSERT.into(Books).columns(['ID']).rows([123])) - const res = await GET(`/browse/Books?$filter=not endswith(title,'Sturm')`) + await cds.run(INSERT.into(Books).columns(['ID']).rows([456])) + const res = await GET(`/browse/Books?$filter=not endswith(title,'höhe')`) 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 })) + expect(res.data.value.some(item => item.ID === 456)).to.be.true + await cds.run(DELETE.from(Books).where({ ID: 456 })) }) test('indexof', async () => { @@ -76,13 +76,13 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) - test('not startswith', async () => { + test('not startswith finds null', async () => { const { Books } = cds.entities('sap.capire.bookshop') - await cds.run(INSERT.into(Books).columns(['ID']).rows([123])) + await cds.run(INSERT.into(Books).columns(['ID']).rows([456])) const res = await GET(`/browse/Books?$filter=not startswith(title,'Sturm')`) 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 })) + expect(res.data.value.some(item => item.ID === 456)).to.be.true + await cds.run(DELETE.from(Books).where({ ID: 456 })) }) test('substring', async () => { From a44e5b5cdc22efe98f3262852039679b91237210 Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 12:01:23 +0100 Subject: [PATCH 6/7] use existing test in SELECT.js --- test/compliance/SELECT.test.js | 6 ++++++ test/scenarios/bookshop/funcs.test.js | 18 ------------------ 2 files changed, 6 insertions(+), 18 deletions(-) diff --git a/test/compliance/SELECT.test.js b/test/compliance/SELECT.test.js index 786193446..03ff8da90 100644 --- a/test/compliance/SELECT.test.js +++ b/test/compliance/SELECT.test.js @@ -474,6 +474,7 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: [CXL`not startswith(string,${'n'})`] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) test('deep nested boolean function w/o operator', async () => { @@ -488,6 +489,7 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: [CXL`not startswith(string,${'n'}) and not startswith(string,${'n'})`] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) test('multiple levels of not negations of expressions', async () => { @@ -495,6 +497,7 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: ['not', { xpr: ['not', CXL`not startswith(string,${'n'})`] }] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) test('multiple not in a single deep nested expression', async () => { @@ -509,6 +512,7 @@ describe('SELECT', () => { throw err } assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) }) @@ -517,6 +521,7 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: ['not', { xpr: ['not', CXL`not startswith(string,${'n'}) and not startswith(string,${'n'})`] }] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) test('multiple levels of not negations of expression with multiple not in a single expression', async () => { @@ -531,6 +536,7 @@ describe('SELECT', () => { throw err } assert.strictEqual(res[0].string, 'yes') + assert.strictEqual(res[1].string, null) }) }) diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index ef177bdc0..b634f101b 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -42,15 +42,6 @@ describe('Bookshop - Functions', () => { expect(wrong.data.value.length).to.be.eq(0) }) - test('not endswith findes null', async () => { - const { Books } = cds.entities('sap.capire.bookshop') - await cds.run(INSERT.into(Books).columns(['ID']).rows([456])) - const res = await GET(`/browse/Books?$filter=not endswith(title,'höhe')`) - expect(res.status).to.be.eq(200) - expect(res.data.value.some(item => item.ID === 456)).to.be.true - await cds.run(DELETE.from(Books).where({ ID: 456 })) - }) - test('indexof', async () => { const res = await GET(`/browse/Books?$filter=indexof(author,'Allen') eq 6`) @@ -76,15 +67,6 @@ 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.into(Books).columns(['ID']).rows([456])) - const res = await GET(`/browse/Books?$filter=not startswith(title,'Sturm')`) - expect(res.status).to.be.eq(200) - expect(res.data.value.some(item => item.ID === 456)).to.be.true - await cds.run(DELETE.from(Books).where({ ID: 456 })) - }) - test('substring', async () => { const [three, two, negative] = await Promise.all([ GET(`/browse/Books?$filter=substring(author,1,2) eq 'dg'`), From eb9138be7fed77cdd9e4310ac7ff3c85ff7bb9bd Mon Sep 17 00:00:00 2001 From: I543501 <56645452+larslutz96@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:34:18 +0100 Subject: [PATCH 7/7] add postgres coalesce --- postgres/lib/cql-functions.js | 4 ++-- test/compliance/SELECT.test.js | 6 ------ test/scenarios/bookshop/funcs.test.js | 18 ++++++++++++++++++ 3 files changed, 20 insertions(+), 8 deletions(-) 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/compliance/SELECT.test.js b/test/compliance/SELECT.test.js index 03ff8da90..786193446 100644 --- a/test/compliance/SELECT.test.js +++ b/test/compliance/SELECT.test.js @@ -474,7 +474,6 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: [CXL`not startswith(string,${'n'})`] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) test('deep nested boolean function w/o operator', async () => { @@ -489,7 +488,6 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: [CXL`not startswith(string,${'n'}) and not startswith(string,${'n'})`] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) test('multiple levels of not negations of expressions', async () => { @@ -497,7 +495,6 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: ['not', { xpr: ['not', CXL`not startswith(string,${'n'})`] }] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) test('multiple not in a single deep nested expression', async () => { @@ -512,7 +509,6 @@ describe('SELECT', () => { throw err } assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) }) @@ -521,7 +517,6 @@ describe('SELECT', () => { const query = CQL`SELECT * FROM ${string} WHERE ${{ xpr: ['not', { xpr: ['not', CXL`not startswith(string,${'n'}) and not startswith(string,${'n'})`] }] }} ORDER BY string DESC` const res = await cds.run(query) assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) test('multiple levels of not negations of expression with multiple not in a single expression', async () => { @@ -536,7 +531,6 @@ describe('SELECT', () => { throw err } assert.strictEqual(res[0].string, 'yes') - assert.strictEqual(res[1].string, null) }) }) diff --git a/test/scenarios/bookshop/funcs.test.js b/test/scenarios/bookshop/funcs.test.js index b634f101b..585a5daee 100644 --- a/test/scenarios/bookshop/funcs.test.js +++ b/test/scenarios/bookshop/funcs.test.js @@ -42,6 +42,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`) @@ -67,6 +76,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'`),