From 25590a0372f7bca3f966502015648f3a1a9a6a0b Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Wed, 15 Nov 2023 11:42:24 +0100 Subject: [PATCH 01/10] Initial implementation for stream entries --- db-service/lib/cqn2sql.js | 51 +++++++++++++++++++++++++++--- sqlite/test/general/stream.test.js | 14 ++++---- 2 files changed, 54 insertions(+), 11 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index c74703bd5..0b6ee9f1d 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -423,12 +423,55 @@ class CQN2SQLRenderer { .filter(a => a) .map(c => c.sql) + // REVISIT: yield less often + const stringify = async function* (entries) { + yield '[' + + let sep = '' + for (const row of entries) { + if (!sep) sep = ',' + else yield sep + + let sepsub = '' + yield '{' + for (const key in row) { + + if (!sepsub) sepsub = ',' + else yield sepsub + + yield JSON.stringify(key) + yield ':' + + const val = row[key] + if (val instanceof Readable) { + yield '"' + + // TODO: double check that it works + val.setEncoding('base64') + for await (const chunk of val) { + yield chunk + } + + yield '"' + } else { + yield JSON.stringify(val) + } + } + yield '}' + } + + yield ']' + } + // Include this.values for placeholders /** @type {unknown[][]} */ - this.entries = [[...this.values, JSON.stringify(INSERT.entries)]] - return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${ - this.columns - }) SELECT ${extraction} FROM json_each(?)`) + this.entries = [[...this.values, + INSERT.entries instanceof Readable + ? INSERT.entries + : Readable.from(stringify(INSERT.entries)) + ]] + return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns + }) SELECT ${extraction} FROM json_each(?)`) } /** diff --git a/sqlite/test/general/stream.test.js b/sqlite/test/general/stream.test.js index fb883167b..473ab64c4 100644 --- a/sqlite/test/general/stream.test.js +++ b/sqlite/test/general/stream.test.js @@ -21,11 +21,11 @@ describe('STREAM', () => { describe('cds.stream', () => { beforeAll(async () => { - const data = fs.readFileSync(path.join(__dirname, 'samples/test.jpg')) - await cds.run('INSERT INTO test_Images values(?,?)', [ - [1, data], - [2, null], - ]) + let data = fs.createReadStream(path.join(__dirname, 'samples/test.jpg')) + await INSERT([ + { data: data, ID: 1 }, + { data: null, ID: 2 }, + ]).into('test.Images') }) afterAll(async () => { @@ -236,7 +236,7 @@ describe('STREAM', () => { const { Images } = cds.entities('test') const stream = fs.createReadStream(path.join(__dirname, 'samples/data.json')) - const changes = await STREAM.into(Images).data(stream) + const changes = await INSERT.into(Images).entries(stream) try { expect(changes).toEqual(2) } catch (e) { @@ -246,7 +246,7 @@ describe('STREAM', () => { const out1000 = fs.createWriteStream(path.join(__dirname, 'samples/1000.png')) const out1001 = fs.createWriteStream(path.join(__dirname, 'samples/1001.png')) - const in1000 = await STREAM.from(Images, { ID: 1000 }).column('data') + const in1000 = (await SELECT.one.from(Images, { ID: 1000 }).columns('data')).data const in1001 = await STREAM.from(Images, { ID: 1001 }).column('data') in1000.pipe(out1000) From 236d17a4c2dbd328502d1b4292489e364e3ed164 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Wed, 15 Nov 2023 14:02:10 +0100 Subject: [PATCH 02/10] Adjust HANA to new entries stream logic --- db-service/lib/cqn2sql.js | 86 +++++++++++++++--------------- hana/lib/HANAService.js | 8 ++- sqlite/test/general/stream.test.js | 2 +- 3 files changed, 50 insertions(+), 46 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 0b6ee9f1d..5ffcdabb3 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -423,57 +423,57 @@ class CQN2SQLRenderer { .filter(a => a) .map(c => c.sql) - // REVISIT: yield less often - const stringify = async function* (entries) { - yield '[' - - let sep = '' - for (const row of entries) { - if (!sep) sep = ',' - else yield sep - - let sepsub = '' - yield '{' - for (const key in row) { - - if (!sepsub) sepsub = ',' - else yield sepsub - - yield JSON.stringify(key) - yield ':' - - const val = row[key] - if (val instanceof Readable) { - yield '"' - - // TODO: double check that it works - val.setEncoding('base64') - for await (const chunk of val) { - yield chunk - } - - yield '"' - } else { - yield JSON.stringify(val) - } - } - yield '}' - } - - yield ']' - } - // Include this.values for placeholders /** @type {unknown[][]} */ this.entries = [[...this.values, - INSERT.entries instanceof Readable - ? INSERT.entries - : Readable.from(stringify(INSERT.entries)) + INSERT.entries[0] instanceof Readable + ? INSERT.entries[0] + : Readable.from(this.INSERT_entries_stream(INSERT.entries)) ]] return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns }) SELECT ${extraction} FROM json_each(?)`) } + // REVISIT: yield less often + async *INSERT_entries_stream(entries) { + yield '[' + + let sep = '' + for (const row of entries) { + if (!sep) sep = ',' + else yield sep + + let sepsub = '' + yield '{' + for (const key in row) { + + if (!sepsub) sepsub = ',' + else yield sepsub + + yield JSON.stringify(key) + yield ':' + + const val = row[key] + if (val instanceof Readable) { + yield '"' + + // TODO: double check that it works + val.setEncoding('base64') + for await (const chunk of val) { + yield chunk + } + + yield '"' + } else { + yield JSON.stringify(val) + } + } + yield '}' + } + + yield ']' + } + /** * Renders an INSERT query with rows property * @param {import('./infer/cqn').INSERT} q diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 0212a9b42..5b77a80b8 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -95,7 +95,7 @@ class HANAService extends SQLService { if (!sql) return // Do nothing when there is nothing to be done const ps = await this.prepare(sql) // HANA driver supports batch execution - const results = entries ? await ps.runBatch(entries) : await ps.run() + const results = entries ? await ps.run(entries) : await ps.run() return new this.class.InsertResults(cqn, results) } @@ -570,7 +570,11 @@ class HANAService extends SQLService { }) cur[0] += ']' } else { - this.entries = [[JSON.stringify(INSERT.entries)]] + this.entries = [ + INSERT.entries[0] instanceof Readable + ? INSERT.entries[0] + : Readable.from(this.INSERT_entries_stream(INSERT.entries)) + ] } // WITH SRC is used to force HANA to interpret the ? as a NCLOB allowing for streaming of the data diff --git a/sqlite/test/general/stream.test.js b/sqlite/test/general/stream.test.js index 473ab64c4..70e420879 100644 --- a/sqlite/test/general/stream.test.js +++ b/sqlite/test/general/stream.test.js @@ -246,7 +246,7 @@ describe('STREAM', () => { const out1000 = fs.createWriteStream(path.join(__dirname, 'samples/1000.png')) const out1001 = fs.createWriteStream(path.join(__dirname, 'samples/1001.png')) - const in1000 = (await SELECT.one.from(Images, { ID: 1000 }).columns('data')).data + const in1000 = await STREAM.from(Images, { ID: 1000 }).column('data') const in1001 = await STREAM.from(Images, { ID: 1001 }).column('data') in1000.pipe(out1000) From 6a867c0f05f5e8b878cf05ce7a905d28812ae8da Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Thu, 16 Nov 2023 10:43:39 +0100 Subject: [PATCH 03/10] Adjust HANAService for HANA SP2 --- hana/lib/HANAService.js | 34 +++++++--------------------------- 1 file changed, 7 insertions(+), 27 deletions(-) diff --git a/hana/lib/HANAService.js b/hana/lib/HANAService.js index 5b77a80b8..a1f69cfa5 100644 --- a/hana/lib/HANAService.js +++ b/hana/lib/HANAService.js @@ -86,16 +86,15 @@ class HANAService extends SQLService { } async onINSERT({ query, data }) { - // Using runBatch for HANA 2.0 and lower sometimes leads to integer underflow errors - // REVISIT: Address runBatch issues in node-hdb and hana-client - if (HANAVERSION <= 2) { - return super.onINSERT(...arguments) - } const { sql, entries, cqn } = this.cqn2sql(query, data) if (!sql) return // Do nothing when there is nothing to be done const ps = await this.prepare(sql) // HANA driver supports batch execution - const results = entries ? await ps.run(entries) : await ps.run() + const results = await (entries + ? HANAVERSION <= 2 + ? entries.reduce((l, c) => l.then(() => ps.run(c)), Promise.resolve(0)) + : ps.run(entries) + : ps.run()) return new this.class.InsertResults(cqn, results) } @@ -549,32 +548,13 @@ class HANAService extends SQLService { // HANA Express does not process large JSON documents // The limit is somewhere between 64KB and 128KB if (HANAVERSION <= 2) { - // Simple line splitting would be preferred, but batch execute does not work properly - // Which makes sending every line separately much slower - // this.entries = INSERT.entries.map(e => [JSON.stringify(e)]) - - this.entries = [] - let cur = ['['] - this.entries.push(cur) - INSERT.entries - .map(r => JSON.stringify(r)) - .forEach(r => { - if (cur[0].length > 65535) { - cur[0] += ']' - cur = ['['] - this.entries.push(cur) - } else if (cur[0].length > 1) { - cur[0] += ',' - } - cur[0] += r - }) - cur[0] += ']' + this.entries = INSERT.entries.map(e => (e instanceof Readable ? [e] : [Readable.from(this.INSERT_entries_stream([e]))])) } else { this.entries = [ INSERT.entries[0] instanceof Readable ? INSERT.entries[0] : Readable.from(this.INSERT_entries_stream(INSERT.entries)) - ] + ] } // WITH SRC is used to force HANA to interpret the ? as a NCLOB allowing for streaming of the data From f5793c59d1067137a3680eaddceed5b9947b8f07 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Tue, 21 Nov 2023 11:30:49 +0100 Subject: [PATCH 04/10] Align rows with entries and test expectations --- db-service/lib/cqn2sql.js | 57 ++++++++++++++++++++++---- db-service/test/cqn2sql/insert.test.js | 22 +++++----- db-service/test/cqn2sql/upsert.test.js | 14 ++++--- 3 files changed, 70 insertions(+), 23 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 5ffcdabb3..a9068d54c 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -465,7 +465,7 @@ class CQN2SQLRenderer { yield '"' } else { - yield JSON.stringify(val) + yield val === undefined ? 'null' : JSON.stringify(val) } } yield '}' @@ -474,6 +474,43 @@ class CQN2SQLRenderer { yield ']' } + // REVISIT: yield less often + async *INSERT_rows_stream(entries) { + yield '[' + + let sep = '' + for (const row of entries) { + if (!sep) sep = ',' + else yield sep + + let sepsub = '' + yield '[' + for (let key = 0; key < row.length; key++) { + + if (!sepsub) sepsub = ',' + else yield sepsub + + const val = row[key] + if (val instanceof Readable) { + yield '"' + + // TODO: double check that it works + val.setEncoding('base64') + for await (const chunk of val) { + yield chunk + } + + yield '"' + } else { + yield val === undefined ? 'null' : JSON.stringify(val) + } + } + yield ']' + } + + yield ']' + } + /** * Renders an INSERT query with rows property * @param {import('./infer/cqn').INSERT} q @@ -485,21 +522,27 @@ class CQN2SQLRenderer { const alias = INSERT.into.as const elements = q.elements || q.target?.elements const columns = INSERT.columns - || cds.error`Cannot insert rows without columns or elements` + || cds.error`Cannot insert rows without columns or elements` const inputConverter = this.class._convertInput - const extraction = columns.map((c,i) => { + const extraction = columns.map((c, i) => { const extract = `value->>'$[${i}]'` const element = elements?.[c] const converter = element?.[inputConverter] - return converter?.(extract,element) || extract + return converter?.(extract, element) || extract }) this.columns = columns.map(c => this.quote(c)) this.entries = [[JSON.stringify(INSERT.rows)]] - return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${ - this.columns - }) SELECT ${extraction} FROM json_each(?)`) + + this.entries = [[...this.values, + INSERT.rows instanceof Readable + ? INSERT.rows + : Readable.from(this.INSERT_rows_stream(INSERT.rows)) + ]] + + return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns + }) SELECT ${extraction} FROM json_each(?)`) } /** diff --git a/db-service/test/cqn2sql/insert.test.js b/db-service/test/cqn2sql/insert.test.js index d151ccdc7..40a7ff0ac 100644 --- a/db-service/test/cqn2sql/insert.test.js +++ b/db-service/test/cqn2sql/insert.test.js @@ -1,4 +1,6 @@ 'use strict' +const { text } = require('stream/consumers') + const cds = require('@sap/cds/lib') const cqn2sql = require('../../lib/cqn2sql') @@ -9,7 +11,7 @@ beforeAll(async () => { describe('insert', () => { describe('insert only', () => { // Values are missing - test('test with insert values into columns', () => { + test('test with insert values into columns', async () => { const cqnInsert = { INSERT: { into: { ref: ['Foo'] }, @@ -19,10 +21,10 @@ describe('insert', () => { } const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) - test('test with insert rows into columns', () => { + test('test with insert rows into columns', async () => { const cqnInsert = { INSERT: { into: { ref: ['Foo'] }, @@ -34,11 +36,11 @@ describe('insert', () => { }, } const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) // no filtering in INSERT - xtest('test filter in insert rows into columns with not existing column', () => { + xtest('test filter in insert rows into columns with not existing column', async () => { const cqnInsert = { INSERT: { into: { ref: ['Foo2'] }, @@ -50,10 +52,10 @@ describe('insert', () => { }, } const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) - test('test with insert entries', () => { + test('test with insert entries', async () => { const cqnInsert = { INSERT: { into: 'Foo2', @@ -65,10 +67,10 @@ describe('insert', () => { } const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) - test('test with insert with alias', () => { + test('test with insert with alias', async () => { const cqnInsert = { INSERT: { into: { ref: ['Foo2'], as: 'Fooooo2' }, @@ -80,7 +82,7 @@ describe('insert', () => { } const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) }) diff --git a/db-service/test/cqn2sql/upsert.test.js b/db-service/test/cqn2sql/upsert.test.js index df6b0c18b..4e929493f 100644 --- a/db-service/test/cqn2sql/upsert.test.js +++ b/db-service/test/cqn2sql/upsert.test.js @@ -1,4 +1,6 @@ 'use strict' +const { text } = require('stream/consumers') + const cds = require('@sap/cds/lib') const cqn2sql = require('../../lib/cqn2sql') @@ -7,7 +9,7 @@ beforeAll(async () => { }) describe('upsert', () => { - test('test with keys only', () => { + test('test with keys only', async () => { const cqnUpsert = { UPSERT: { into: 'Foo2', @@ -17,11 +19,11 @@ describe('upsert', () => { } const { sql, entries } = cqn2sql(cqnUpsert) - expect({ sql, entries }).toMatchSnapshot() + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) - test('test with entries', () => { - const cqnInsert = { + test('test with entries', async () => { + const cqnUpsert = { UPSERT: { into: 'Foo2', entries: [ @@ -31,7 +33,7 @@ describe('upsert', () => { }, } - const { sql, entries } = cqn2sql(cqnInsert) - expect({ sql, entries }).toMatchSnapshot() + const { sql, entries } = cqn2sql(cqnUpsert) + expect({ sql, entries: [[await text(entries[0][0])]] }).toMatchSnapshot() }) }) From 83a123922f67eb88742640fdcf665ec07154dcfe Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Tue, 21 Nov 2023 14:51:10 +0100 Subject: [PATCH 05/10] Reduce number of yield calls --- db-service/lib/cqn2sql.js | 68 ++++++++++++++++++++++----------------- 1 file changed, 39 insertions(+), 29 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 5f43c5540..82bec2cfa 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -434,81 +434,91 @@ class CQN2SQLRenderer { }) SELECT ${extraction} FROM json_each(?)`) } - // REVISIT: yield less often async *INSERT_entries_stream(entries) { - yield '[' + const bufferLimit = 1 << 16 + let buffer = '[' let sep = '' for (const row of entries) { + buffer += `${sep}{` if (!sep) sep = ',' - else yield sep let sepsub = '' - yield '{' for (const key in row) { - + const keyJSON = `${sepsub}${JSON.stringify(key)}:` if (!sepsub) sepsub = ',' - else yield sepsub - - yield JSON.stringify(key) - yield ':' const val = row[key] if (val instanceof Readable) { - yield '"' + buffer += `${keyJSON}"` // TODO: double check that it works val.setEncoding('base64') for await (const chunk of val) { - yield chunk + buffer += chunk + if (buffer.length > bufferLimit) { + yield buffer + buffer = '' + } } - yield '"' + buffer += '"' } else { - yield val === undefined ? 'null' : JSON.stringify(val) + buffer += `${keyJSON}${val === undefined ? 'null' : JSON.stringify(val)}` } } - yield '}' + buffer += '}' + if (buffer.length > bufferLimit) { + yield buffer + buffer = '' + } } - yield ']' + buffer += ']' + yield buffer } - // REVISIT: yield less often async *INSERT_rows_stream(entries) { - yield '[' + const bufferLimit = 1 << 16 + let buffer = '[' let sep = '' for (const row of entries) { + buffer += `${sep}[` if (!sep) sep = ',' - else yield sep let sepsub = '' - yield '[' for (let key = 0; key < row.length; key++) { - - if (!sepsub) sepsub = ',' - else yield sepsub - const val = row[key] if (val instanceof Readable) { - yield '"' + buffer += `${sepsub}"` // TODO: double check that it works val.setEncoding('base64') for await (const chunk of val) { - yield chunk + buffer += chunk + if (buffer.length > bufferLimit) { + yield buffer + buffer = '' + } } - yield '"' + buffer += '"' } else { - yield val === undefined ? 'null' : JSON.stringify(val) + buffer += `${sepsub}${val === undefined ? 'null' : JSON.stringify(val)}` } + + if (!sepsub) sepsub = ',' + } + buffer += ']' + if (buffer.length > bufferLimit) { + yield buffer + buffer = '' } - yield ']' } - yield ']' + buffer += ']' + yield buffer } /** From 99cec018826c65d5f924502c41b13e74c01b4d8e Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Fri, 1 Dec 2023 10:05:32 +0100 Subject: [PATCH 06/10] Adjust entries stream for postgres --- db-service/lib/cqn2sql.js | 29 ++++++++++++++++++----------- postgres/lib/PostgresService.js | 9 +++++---- 2 files changed, 23 insertions(+), 15 deletions(-) diff --git a/db-service/lib/cqn2sql.js b/db-service/lib/cqn2sql.js index 929bedda8..78190f1fe 100644 --- a/db-service/lib/cqn2sql.js +++ b/db-service/lib/cqn2sql.js @@ -426,11 +426,16 @@ class CQN2SQLRenderer { // Include this.values for placeholders /** @type {unknown[][]} */ - this.entries = [[...this.values, - INSERT.entries[0] instanceof Readable - ? INSERT.entries[0] - : Readable.from(this.INSERT_entries_stream(INSERT.entries)) - ]] + this.entries = [] + if (INSERT.entries[0] instanceof Readable) { + INSERT.entries[0].type = 'json' + this.entries = [[...this.values, INSERT.entries[0]]] + } else { + const stream = Readable.from(this.INSERT_entries_stream(INSERT.entries)) + stream.type = 'json' + this.entries = [[...this.values, stream]] + } + return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns }) SELECT ${extraction} FROM json_each(?)`) } @@ -544,13 +549,15 @@ class CQN2SQLRenderer { }) this.columns = columns.map(c => this.quote(c)) - this.entries = [[JSON.stringify(INSERT.rows)]] - this.entries = [[...this.values, - INSERT.rows instanceof Readable - ? INSERT.rows - : Readable.from(this.INSERT_rows_stream(INSERT.rows)) - ]] + if (INSERT.rows[0] instanceof Readable) { + INSERT.rows[0].type = 'json' + this.entries = [[...this.values, INSERT.rows[0]]] + } else { + const stream = Readable.from(this.INSERT_rows_stream(INSERT.rows)) + stream.type = 'json' + this.entries = [[...this.values, stream]] + } return (this.sql = `INSERT INTO ${this.quote(entity)}${alias ? ' as ' + this.quote(alias) : ''} (${this.columns }) SELECT ${extraction} FROM json_each(?)`) diff --git a/postgres/lib/PostgresService.js b/postgres/lib/PostgresService.js index 424d32a08..051c86bc5 100644 --- a/postgres/lib/PostgresService.js +++ b/postgres/lib/PostgresService.js @@ -135,12 +135,13 @@ GROUP BY k } prepare(sql) { - const query = { + // Track queries name for postgres referencing prepare statements + // sha1 as it needs to be less then 63 character + const sha = crypto.createHash('sha1').update(sql).digest('hex') + const query = this._queryCache[sha] = this._queryCache[sha] || { _streams: 0, text: sql, - // Track queries name for postgres referencing prepare statements - // sha1 as it needs to be less then 63 characters - name: crypto.createHash('sha1').update(sql).digest('hex'), + name: sha, } return { run: async values => { From 2a689dcf94bec5bae15f80a022c80dcaa5395765 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Fri, 1 Dec 2023 10:15:35 +0100 Subject: [PATCH 07/10] Add missing cache initialization --- postgres/lib/PostgresService.js | 1 + 1 file changed, 1 insertion(+) diff --git a/postgres/lib/PostgresService.js b/postgres/lib/PostgresService.js index 051c86bc5..cdbd8091b 100644 --- a/postgres/lib/PostgresService.js +++ b/postgres/lib/PostgresService.js @@ -13,6 +13,7 @@ class PostgresService extends SQLService { cds.options.dialect = 'postgres' } this.kind = 'postgres' + this._queryCache = {} return super.init(...arguments) } From c46eba45e7fb4bb332874fd422cabb02a5863ca0 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Mon, 4 Dec 2023 10:28:00 +0100 Subject: [PATCH 08/10] Add sort to genres read after write test --- test/scenarios/bookshop/genres.test.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/test/scenarios/bookshop/genres.test.js b/test/scenarios/bookshop/genres.test.js index eba388dac..d421b918f 100644 --- a/test/scenarios/bookshop/genres.test.js +++ b/test/scenarios/bookshop/genres.test.js @@ -35,6 +35,17 @@ describe('Bookshop - Genres', () => { delete insertResponse.data['@odata.context'] const assert = require('assert') + + // Read after write does not sort the results + // therefor asynchronious databases might return in different orders + const sort = (a, b) => { + if (!b?.children) return + const order = b.children.reduce((l, c, i) => { l[c.ID] = i; return l }, {}) + b.children.sort((a, b) => order[a.ID] - order[b.ID]) + b.children.forEach((c, i) => sort(c, a.children[i])) + } + + sort(insertResponse.data, body) assert.deepEqual(insertResponse.data, body) // REVISIT clean up so the deep update test does not fail From 8f6dfc17223a3c14a1491a5d22e7e974259a50b7 Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Mon, 4 Dec 2023 10:37:52 +0100 Subject: [PATCH 09/10] Adjust sort relation --- test/scenarios/bookshop/update.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scenarios/bookshop/update.test.js b/test/scenarios/bookshop/update.test.js index 28fdcd9ff..d9eeb7151 100644 --- a/test/scenarios/bookshop/update.test.js +++ b/test/scenarios/bookshop/update.test.js @@ -54,7 +54,7 @@ describe('Bookshop - Update', () => { expect(affectedRows).to.be.eq(0) }) - test('Update with path expressions', async () => { + xtest('Update with path expressions', async () => { const updateRichardsBooks = UPDATE.entity('AdminService.RenameKeys') .where(`author.name = 'Richard Carpenter'`) .set('ID = 42') From 2b25b8c0de6ef7df26a3996884d701fc54dcee9d Mon Sep 17 00:00:00 2001 From: Bob den Os Date: Mon, 4 Dec 2023 12:01:39 +0100 Subject: [PATCH 10/10] Adjust sort relation --- test/scenarios/bookshop/genres.test.js | 6 +++--- test/scenarios/bookshop/update.test.js | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/scenarios/bookshop/genres.test.js b/test/scenarios/bookshop/genres.test.js index d421b918f..a644caf0f 100644 --- a/test/scenarios/bookshop/genres.test.js +++ b/test/scenarios/bookshop/genres.test.js @@ -39,10 +39,10 @@ describe('Bookshop - Genres', () => { // Read after write does not sort the results // therefor asynchronious databases might return in different orders const sort = (a, b) => { - if (!b?.children) return + if (!a?.children || !b?.children) return const order = b.children.reduce((l, c, i) => { l[c.ID] = i; return l }, {}) - b.children.sort((a, b) => order[a.ID] - order[b.ID]) - b.children.forEach((c, i) => sort(c, a.children[i])) + a.children.sort((a, b) => order[a.ID] - order[b.ID]) + a.children.forEach((c, i) => sort(c, b.children[i])) } sort(insertResponse.data, body) diff --git a/test/scenarios/bookshop/update.test.js b/test/scenarios/bookshop/update.test.js index d9eeb7151..28fdcd9ff 100644 --- a/test/scenarios/bookshop/update.test.js +++ b/test/scenarios/bookshop/update.test.js @@ -54,7 +54,7 @@ describe('Bookshop - Update', () => { expect(affectedRows).to.be.eq(0) }) - xtest('Update with path expressions', async () => { + test('Update with path expressions', async () => { const updateRichardsBooks = UPDATE.entity('AdminService.RenameKeys') .where(`author.name = 'Richard Carpenter'`) .set('ID = 42')