diff --git a/db-service/lib/SQLService.js b/db-service/lib/SQLService.js index 117a5fe5b..a34730592 100644 --- a/db-service/lib/SQLService.js +++ b/db-service/lib/SQLService.js @@ -21,6 +21,14 @@ const BINARY_TYPES = { class SQLService extends DatabaseService { init() { + this.on(['UPDATE', 'DELETE'], ({ query}, next) => { + const cqn = query.UPDATE ?? query.DELETE + if (!cqn.where && !cqn.from?.ref?.at(-1).where && !cqn.entity?.ref?.at(-1).where) { + const op = query.DELETE ? 'delete' : 'update' + return cds.error(`Trying to ${op} all entites. Call .where(...) explicitely, to ${op} all entities.`) + } + return next() + }) this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./fill-in-keys')) // REVISIT should be replaced by correct input processing eventually this.on(['INSERT', 'UPSERT', 'UPDATE'], require('./deep-queries').onDeep) if (cds.env.features.db_strict) { diff --git a/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap b/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap index 98c13e2e9..4039a2090 100644 --- a/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap +++ b/db-service/test/cqn2sql/__snapshots__/delete.test.js.snap @@ -6,7 +6,7 @@ exports[`delete test simple cascade delete for entity with 'not exists' 1`] = `" exports[`delete test simple cascade delete for entity with 'not in' 1`] = `"DELETE FROM Foo as Foo WHERE Foo.x not in (SELECT Foo2.a FROM Foo2 as Foo2)"`; -exports[`delete test with from entity 1`] = `"DELETE FROM Foo as Foo"`; +exports[`delete test with from entity 1`] = `"DELETE FROM Foo as Foo WHERE ? = ?"`; exports[`delete test with from ref 1`] = `"DELETE FROM Foo as Foo"`; diff --git a/db-service/test/cqn2sql/delete.test.js b/db-service/test/cqn2sql/delete.test.js index 2b89725e7..c47831fa1 100644 --- a/db-service/test/cqn2sql/delete.test.js +++ b/db-service/test/cqn2sql/delete.test.js @@ -10,7 +10,7 @@ beforeAll(async () => { }) describe('delete', () => { test('test with from entity', () => { - const cqnDelete = DELETE.from(cds.model.definitions.Foo) + const cqnDelete = DELETE.from(cds.model.definitions.Foo).where('1=1') const { sql } = cqn2sql(cqnDelete) expect(sql).toMatchSnapshot() }) diff --git a/db-service/test/cqn4sql/DELETE.test.js b/db-service/test/cqn4sql/DELETE.test.js index fa772a502..7b11cc676 100644 --- a/db-service/test/cqn4sql/DELETE.test.js +++ b/db-service/test/cqn4sql/DELETE.test.js @@ -21,7 +21,7 @@ describe('DELETE', () => { it('DELETE with where exists expansion', () => { const { DELETE } = cds.ql - let d = DELETE.from('bookshop.Books:author') + let d = DELETE.from('bookshop.Books:author').where('1=1') const query = cqn4sql(d, model) // how to express this in CQN? // DELETE.from({ref: ['bookshop.Authors'], as: 'author'}).where('exists ( SELECT 1 from bookshop.Books as Books where author_ID = author.ID)') @@ -186,7 +186,7 @@ describe('DELETE', () => { it('DELETE with assoc filter and where exists expansion', () => { const { DELETE } = cds.ql - let d = DELETE.from('bookshop.Reproduce[author = null and ID = 99]:accessGroup') + let d = DELETE.from('bookshop.Reproduce[author = null and ID = 99]:accessGroup').where('1=1') const query = cqn4sql(d, model) const expected = { diff --git a/postgres/test/ql.test.js b/postgres/test/ql.test.js index 4ad6d1489..3507b70d3 100644 --- a/postgres/test/ql.test.js +++ b/postgres/test/ql.test.js @@ -180,7 +180,7 @@ describe('QL to PostgreSQL', () => { test('-> multiple rows', async () => { const { Beers } = cds.entities('csw') - const affectedRows = await cds.run(UPDATE(Beers).set({ abv: 1.0 })) + const affectedRows = await cds.run(UPDATE(Beers).set({ abv: 1.0 })).where('1=1') expect(affectedRows).to.equal(11) }) }) diff --git a/sqlite/test/general/stream.compat.test.js b/sqlite/test/general/stream.compat.test.js index 8e2b0e2f4..832e9264b 100644 --- a/sqlite/test/general/stream.compat.test.js +++ b/sqlite/test/general/stream.compat.test.js @@ -28,7 +28,7 @@ describe('streaming', () => { afterAll(async () => { const { Images } = cds.entities('test') - await DELETE.from(Images) + await DELETE.from(Images).where('1=1') }) describe('READ', () => { diff --git a/sqlite/test/general/stream.test.js b/sqlite/test/general/stream.test.js index d6bc1302c..16331ef3a 100644 --- a/sqlite/test/general/stream.test.js +++ b/sqlite/test/general/stream.test.js @@ -26,7 +26,7 @@ describe('streaming', () => { afterAll(async () => { const { Images } = cds.entities('test') - await DELETE.from(Images) + await DELETE.from(Images).where('1=1') }) describe('READ', () => { diff --git a/sqlite/test/general/uuid.test.js b/sqlite/test/general/uuid.test.js index 2c449cbba..247eff816 100644 --- a/sqlite/test/general/uuid.test.js +++ b/sqlite/test/general/uuid.test.js @@ -10,7 +10,7 @@ describe('UUID Generation', () => { const result = await SELECT.from('test.bar') expect(result).toEqual([{ ID: expect.any(String) }]) - await DELETE('test.bar') + await DELETE('test.bar').where('1=1') }) }) test('INSERT with multiple entries', async () => { @@ -22,7 +22,7 @@ describe('UUID Generation', () => { expect(result).toEqual([{ ID: expect.any(String) }, { ID: expect.any(String) }]) expect(result[0].ID).not.toEqual(result[1].ID) - await DELETE('test.bar') + await DELETE('test.bar').where('1=1') }) }) diff --git a/test/compliance/SELECT.test.js b/test/compliance/SELECT.test.js index 0c450c840..8598d8a7d 100644 --- a/test/compliance/SELECT.test.js +++ b/test/compliance/SELECT.test.js @@ -255,7 +255,7 @@ describe('SELECT', () => { const entity = `basic.literals.dateTime` const dateTime = '1970-02-02T10:09:34Z' const timestamp = dateTime.slice(0, -1) + '.000Z' - await DELETE.from(entity) + await DELETE.from(entity).where('1=1') await INSERT({ dateTime }).into(entity) const dateTimeMatches = await SELECT('dateTime').from(entity).where(`dateTime = `, dateTime) assert.strictEqual(dateTimeMatches.length, 1, 'Ensure that the dateTime column matches the dateTime value') diff --git a/test/compliance/api.test.js b/test/compliance/api.test.js index a66f912fc..574344f4b 100644 --- a/test/compliance/api.test.js +++ b/test/compliance/api.test.js @@ -23,7 +23,7 @@ const { expect } = cds.test(__dirname + '/resources') test('Update returns affected rows', async () => { const { count } = await SELECT.one`count(*)`.from('complex.associations.Books') - const affectedRows = await UPDATE.entity('complex.associations.Books').data({title: 'Book'}) + const affectedRows = await UPDATE.entity('complex.associations.Books').data({title: 'Book'}).where('1=1') expect(affectedRows).to.be.eq(count) }) diff --git a/test/scenarios/bookshop/update.test.js b/test/scenarios/bookshop/update.test.js index 0eb66d705..273b2c966 100644 --- a/test/scenarios/bookshop/update.test.js +++ b/test/scenarios/bookshop/update.test.js @@ -124,7 +124,7 @@ describe('Bookshop - Update', () => { test('programmatic update with unique constraint conflict', async () => { const { Genres } = cds.entities('sap.capire.bookshop') - const update = UPDATE(Genres).set('ID = 201') + const update = UPDATE(Genres).set('ID = 201').where('1=1') const err = await expect(update).rejected // Works fine locally, but refuses to function in pipeline // expect(err).to.be.instanceOf(Error)