Skip to content

Commit

Permalink
feat(hana): drop prepared statements after end of transaction (#537)
Browse files Browse the repository at this point in the history
Co-authored-by: Samuel Brucksch <samuel.brucksch@sap.com>
Co-authored-by: Bob den Os <bob.den.os@sap.com>
Co-authored-by: Bob den Os <108393871+BobdenOs@users.noreply.github.com>
  • Loading branch information
4 people authored Mar 21, 2024
1 parent 72ff0ed commit b1f864e
Show file tree
Hide file tree
Showing 5 changed files with 78 additions and 61 deletions.
2 changes: 1 addition & 1 deletion db-service/lib/cqn2sql.js
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ class CQN2SQLRenderer {
DROP(q) {
const { target } = q
const isView = target.query || target.projection
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.name(target.name)}`)
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} IF EXISTS ${this.quote(this.name(target.name))}`)
}

// SELECT Statements ------------------------------------------------
Expand Down
16 changes: 14 additions & 2 deletions hana/lib/HANAService.js
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,10 @@ class HANAService extends SQLService {

// prepare and exec are both implemented inside the drivers
prepare(sql, hasBlobs) {
return this.ensureDBC().prepare(sql, hasBlobs)
const stmt = this.ensureDBC().prepare(sql, hasBlobs)
// we store the statements, to release them on commit/rollback all at once
this.dbc.statements.push(stmt)
return stmt
}

exec(sql) {
Expand Down Expand Up @@ -683,7 +686,7 @@ class HANAService extends SQLService {
DROP(q) {
const { target } = q
const isView = target.query || target.projection
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} ${this.name(target.name)}`)
return (this.sql = `DROP ${isView ? 'VIEW' : 'TABLE'} ${this.quote(this.name(target.name))}`)
}

from_args(args) {
Expand Down Expand Up @@ -1054,16 +1057,25 @@ class HANAService extends SQLService {

onBEGIN() {
DEBUG?.('BEGIN')
if (this.dbc) this.dbc.statements = []
return this.dbc?.begin()
}

onCOMMIT() {
DEBUG?.('COMMIT')
this.dbc?.statements?.forEach(stmt => stmt
.then(stmt => stmt.drop())
.catch(() => { })
)
return this.dbc?.commit()
}

onROLLBACK() {
DEBUG?.('ROLLBACK')
this.dbc?.statements?.forEach(stmt => stmt
.then(stmt => stmt.drop())
.catch(() => { })
)
return this.dbc?.rollback()
}

Expand Down
4 changes: 4 additions & 0 deletions hana/lib/drivers/base.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ class HANADriver {
const stmt = await prep
return prom(stmt, 'exec')(params)
},
drop: async () => {
const stmt = await prep
return stmt.drop()
}
}
}

Expand Down
1 change: 1 addition & 0 deletions hana/lib/drivers/hana-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ class HANAClientDriver extends driver {
rsStreamsProm.resolve = resolve
rsStreamsProm.reject = reject
})
rsStreams.catch(() => {})

rs._rowPosition = -1
const _next = prom(rs, 'next')
Expand Down
116 changes: 58 additions & 58 deletions sqlite/test/general/stream.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,70 +26,70 @@ describe('streaming', () => {
await DELETE.from(Images)
})

test('READ stream property with .from and .where', async () => {
test('READ stream property with .from and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream('data').from(Images).where({ ID: 1 })
await checkSize(stream)
})
}))

test('READ stream property that equals null', async () => {
test('READ stream property that equals null', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream('data').from(Images).where({ ID: 2 })
expect(stream).toBeNull()
})
}))

test('READ stream property with object in .from', async () => {
test('READ stream property with object in .from', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream('data').from(Images, { ID: 1 })
await checkSize(stream)
})
}))

test('READ stream property with key in .from', async () => {
test('READ stream property with key in .from', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream('data').from(Images, 1)
await checkSize(stream)
})
}))

test('READ stream property with .where as alternating string/value arguments list', async () => {
test('READ stream property with .where as alternating string/value arguments list', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream('data').from(Images).where('ID =', 1)
await checkSize(stream)
})
}))

test('READ stream property from entry that does not exist', async () => {
test('READ stream property from entry that does not exist', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
try {
await cds.stream('data').from(Images, 23)
} catch (e) {
expect(e.code).toEqual(404)
}
})
}))

test('READ stream property with key and column in .from', async () => {
test('READ stream property with key and column in .from', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream().from(Images, 1, 'data')
await checkSize(stream)
})
}))

test('READ stream property with column as function in .from', async () => {
test('READ stream property with column as function in .from', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = await cds.stream().from(Images, 1, a => a.data)
await checkSize(stream)
})
}))

test('READ stream property using SELECT CQN', async () => {
test('READ stream property using SELECT CQN', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const cqn = SELECT('data').from(Images, 1)
const stream = await cds.stream(cqn)
await checkSize(stream)
})
}))

test('READ stream property as array using SELECT CQN', async () => {
test('READ stream property as array using SELECT CQN', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const cqn = SELECT('data').from(Images).where('ID =', 1)
const stream = await cds.stream(cqn)
await checkSize(stream)
})
}))
})

describe('Streaming API', () => {
Expand All @@ -109,19 +109,19 @@ describe('streaming', () => {
})

describe('READ', () => {
test('READ stream property with .one .from, .column and .where', async () => {
test('READ stream property with .one .from, .column and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const { data: stream } = await SELECT.one.from(Images).columns('data').where({ ID: 1 })
await checkSize(stream)
})
}))

test('READ stream property with .from, .column and .where', async () => {
test('READ stream property with .from, .column and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const [{ data: stream }] = await SELECT.from(Images).columns('data').where({ ID: 1 })
await checkSize(stream)
})
}))

test('READ stream property with odata $mediaContentType', async () => {
test('READ stream property with odata $mediaContentType', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const {
data: stream, '$mediaContentType': val
Expand All @@ -130,22 +130,22 @@ describe('streaming', () => {
.where({ ID: 1 })
await checkSize(stream)
expect(val).toEqual('image/jpeg')
})
}))

test('READ null stream property with .from, .column and .where', async () => {
test('READ null stream property with .from, .column and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const [{ data: stream }] = await SELECT.from(Images).columns('data').where({ ID: 2 })
expect(stream).toBeNull()
})
}))

test('READ ID and stream property with .from, .column and .where', async () => {
test('READ ID and stream property with .from, .column and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const [{ ID, data: stream }] = await SELECT.from(Images).columns(['ID', 'data']).where({ ID: 1 })
await checkSize(stream)
expect(ID).toEqual(1)
})
}))

test('READ multiple stream properties with .from, .column and .where', async () => {
test('READ multiple stream properties with .from, .column and .where', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const [{
ID, data: stream1, data2: stream2
Expand All @@ -155,9 +155,9 @@ describe('streaming', () => {
await checkSize(stream1)
await checkSize(stream2)
expect(ID).toEqual(1)
})
}))

test('READ all entries with stream property with .from, .column ', async () => {
test('READ all entries with stream property with .from, .column ', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const [
{ ID: ID1, data: stream1, data2: stream2 },
Expand All @@ -173,17 +173,17 @@ describe('streaming', () => {
expect(ID1).toEqual(1)
expect(ID2).toEqual(2)
expect(ID3).toEqual(3)
})
}))

test('READ one ignore stream properties if columns = all', async () => {
test('READ one ignore stream properties if columns = all', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const result = await SELECT.from(Images).where({ ID: 1 })
expect(result[0].ID).toBe(1)
expect(result[0].data).toBeUndefined()
expect(result[0].data2).toBeUndefined()
})
}))

test('READ multiple entries ignore stream properties if columns = all', async () => {
test('READ multiple entries ignore stream properties if columns = all', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const result = await SELECT.from(Images)
expect(result[0].ID).toBe(1)
Expand All @@ -192,31 +192,31 @@ describe('streaming', () => {
expect(result[1].ID).toBe(2)
expect(result[1].data).toBeUndefined()
expect(result[1].data2).toBeUndefined()
})
}))

test('READ ignore stream properties if columns = *', async () => {
test('READ ignore stream properties if columns = *', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const result = await SELECT.from(Images).columns('*').where({ ID: 1 })
expect(result[0].ID).toBe(1)
expect(result[0].data).toBeUndefined()
expect(result[0].data2).toBeUndefined()
})
}))

test('READ all properties from not existing entry', async () => {
test('READ all properties from not existing entry', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const res = await SELECT.from(Images).columns('*').where({ ID: 15 })
expect(res.length).toBe(0)
})
}))

test('READ stream property from not existing entry', async () => {
test('READ stream property from not existing entry', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const res = await SELECT.from(Images).columns('data').where({ ID: 15 })
expect(res.length).toBe(0)
})
}))
})

describe('WRITE', () => {
test('WRITE with incorrect data type results in error', async () => {
test('WRITE with incorrect data type results in error', async () => cds.tx(async () => {
const { Images } = cds.entities('test')

const stream = new Readable({
Expand All @@ -231,9 +231,9 @@ describe('streaming', () => {
} catch (err) {
expect(err.code).toEqual('ERR_INVALID_ARG_TYPE')
}
})
}))

test('WRITE stream property', async () => {
test('WRITE stream property', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream = fs.createReadStream(path.join(__dirname, 'samples/test.jpg'))

Expand All @@ -242,9 +242,9 @@ describe('streaming', () => {

const [{ data2: stream_ }] = await SELECT.from(Images).columns('data2').where({ ID: 3 })
await checkSize(stream_)
})
}))

test('WRITE multiple stream properties', async () => {
test('WRITE multiple stream properties', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const stream1 = fs.createReadStream(path.join(__dirname, 'samples/test.jpg'))
const stream2 = fs.createReadStream(path.join(__dirname, 'samples/test.jpg'))
Expand All @@ -259,9 +259,9 @@ describe('streaming', () => {
.where({ ID: 4 })
await checkSize(stream1_)
await checkSize(stream2_)
})
}))

test('WRITE multiple blob properties', async () => {
test('WRITE multiple blob properties', async () => cds.tx(async () => {
const { Images } = cds.entities('test')
const blob1 = fs.readFileSync(path.join(__dirname, 'samples/test.jpg'))
const blob2 = fs.readFileSync(path.join(__dirname, 'samples/test.jpg'))
Expand All @@ -277,9 +277,9 @@ describe('streaming', () => {
.where({ ID: 4 })
await checkSize(stream1_)
await checkSize(stream2_)
})
}))

test('WRITE stream property on view', async () => {
test('WRITE stream property on view', async () => cds.tx(async () => {
const { ImagesView } = cds.entities('test')
const stream = fs.createReadStream(path.join(__dirname, 'samples/test.jpg'))

Expand All @@ -288,9 +288,9 @@ describe('streaming', () => {

const [{ renamedData: stream_ }] = await SELECT.from(ImagesView).columns('renamedData').where({ ID: 1 })
await checkSize(stream_)
})
}))

test('WRITE dataset from json file stream', async () => {
test('WRITE dataset from json file stream', async () => cds.tx(async () => {
const { Images } = cds.entities('test')

// REVISIT: required proper BASE64_DECODE support from HANA
Expand Down Expand Up @@ -322,9 +322,9 @@ describe('streaming', () => {
})

await Promise.all([wrap(out1000), wrap(out1001)])
})
}))

xtest('WRITE dataset from json generator stream', async () => {
xtest('WRITE dataset from json generator stream', async () => cds.tx(async () => {
const { Images } = cds.entities('test')

const start = 2000
Expand All @@ -348,7 +348,7 @@ describe('streaming', () => {
} catch (e) {
// @sap/hana-client does not allow for returning the number of affected rows
}
})
}))
})
})
})

0 comments on commit b1f864e

Please sign in to comment.