From b3c4a41f91c8d3ca17b7578a039a4569d0e3d632 Mon Sep 17 00:00:00 2001 From: Linjun He Date: Fri, 19 Jun 2026 01:31:38 -0400 Subject: [PATCH 01/13] [INFRA] override diff to 8.0.4 to address GHSA-73rr-hh4g-fpgx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add `diff: 8.0.4` to overrides in package.json; pulls in the patched diff release in place of the 7.0.0 version that mocha 11.7.6 transitively requests via `^7.0.0` - regenerate package-lock.json via `npm install`; `npm audit` now reports 0 vulnerabilities - vulnerability: GHSA-73rr-hh4g-fpgx ("jsdiff has a Denial of Service vulnerability in parsePatch and applyPatch"). diff versions >=6.0.0 <8.0.3 enter an infinite loop / O(n^3) ReDoS when parsing patches whose filename or patch headers contain `\r`, `
`, or `
`, exhausting memory or CPU. Fixed upstream in diff 8.0.3; pinned here to the latest 8.x patch (8.0.4) - affected dependency: diff is a dev-only transitive dependency pulled in exclusively by mocha (used for pretty-printing assertion diffs in failing test output). it is not shipped to consumers of `hdb` and not reachable with attacker-controlled input in our test setup, so real-world exposure is minimal -- the override silences the npm audit alert and aligns with our existing pattern of pinning patched majors via `overrides` (see safer-buffer, serialize-javascript) - mocha 11.7.6 (latest stable) still declares `diff: ^7.0.0`; the fix has only landed in mocha 12 betas, which we do not adopt. The override is the appropriate stable path until a fixed mocha stable releases --- package-lock.json | 6 +++--- package.json | 3 ++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0cb69bb..1552cdb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -495,9 +495,9 @@ } }, "node_modules/diff": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", - "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", "dev": true, "license": "BSD-3-Clause", "engines": { diff --git a/package.json b/package.json index a9a8c5f..ef867e1 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ }, "overrides": { "safer-buffer": "2.1.2", - "serialize-javascript": "7.0.5" + "serialize-javascript": "7.0.5", + "diff": "8.0.4" } } From 968e5378cf072a8057daeef8d853f939d8c34755 Mon Sep 17 00:00:00 2001 From: Linjun He Date: Wed, 8 Jul 2026 15:47:19 -0400 Subject: [PATCH 02/13] [INTERNAL] fix "avaliable" typo in Writer.js comments --- lib/protocol/Writer.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/protocol/Writer.js b/lib/protocol/Writer.js index 1636be5..25b2546 100644 --- a/lib/protocol/Writer.js +++ b/lib/protocol/Writer.js @@ -194,7 +194,7 @@ Writer.prototype.finializeParameters = function finializeParameters( // store lob length in header var length = header.readInt32LE(2); // readable events might not emit for every chunk so we handle all - // avaliable chunks immediately + // available chunks immediately while (chunk !== null) { if (chunk.length > bytesRemainingForLOBs) { cleanup(); @@ -334,7 +334,7 @@ Writer.prototype.finalizeWriteLobRequest = function finalizeWriteLobRequest( // store lob length in header var length = header.readInt32LE(17); // readable events might not emit for every chunk so we handle all - // avaliable chunks immediately + // available chunks immediately while (chunk !== null) { if (chunk.length > bytesRemaining) { cleanup(); From 756b9bd3a73e1ac5c54fecb1d5f882d7a81daf9c Mon Sep 17 00:00:00 2001 From: Linjun He Date: Wed, 8 Jul 2026 15:52:45 -0400 Subject: [PATCH 03/13] [TEST] fix Stringifier tests to drain readable buffer on Node 26 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - change readable handler to loop read() until null so buffered chunks are not stranded when Node 26 emits one readable event per push instead of coalescing multiple pushes - switch assertion from 'finish' to 'end' event so it runs after the readable side has fully drained, not just after writable done - convert nearby var to let/const per project style Background: how a Transform stream signals completion ===================================================== A Transform stream is both a Writable (input side) and a Readable (output side). Each side has its own "done" event: - 'finish' — writable side done: no more write() calls, and all buffered writes have been processed by _transform / _flush. - 'end' — readable side done: consumer has read every chunk the stream will ever produce, and the buffer is now empty. 'finish' always fires first; 'end' fires only after the readable buffer is fully drained by the consumer. Producer side Consumer side (writable) (readable) ----------- ---------- write(0) --> _transform --push('[0')--> +--------+ | buffer | write(1) --> _transform --push(',1')--> | [0 | | ,1 | write(2) --> _transform --push(',2')--> | ,2 | | ] | end() --> _flush --push( ']')--> +--------+ | | v | +--------+ | | finish | <-- writable done | +--------+ (no more input) | | consumer drains buffer | via read() loop | v +----------+ | buffer | | drained | | + EOF | +----------+ | v +--------+ | end | <-- readable done +--------+ How 'readable' events are scheduled: Node < 26 vs Node 26 --------------------------------------------------------- Node < 26 — pushes coalesce into one 'readable': push('[0') ┐ push(',1') | push(',2') | all 4 pushes land in buffer push(']') ┘ before microtasks flush | v +----------------------+ | ONE 'readable' fires | +----------------------+ | v read() --> "[0,1,2]" (all 4 concatenated) read() --> null (buffer empty) | v 'finish' data == "[0,1,2]" ✓ The buggy single-read-per-event handler happened to work because one read() call retrieved everything. Node 26 — each push tends to fire its own 'readable': push('[0') --> 'readable' no.1 --> read() --> "[0" push(',1') --> 'readable' no.2 --> read() --> ",1" push(',2') --> 'readable' no.3 --> (queued, not yet delivered) push(']') --> 'readable' no.4 --> (queued, not yet delivered) | v 'finish' fires here | v handler runs assertion: data == "[0,1" ✗ | v (later) events no.3, no.4 deliver — too late Only two chunks reach the accumulator; the remaining two are still in the readable buffer when 'finish' fires. JSON.parse sees a truncated string and throws. The fix ------- Draining with \`while ((chunk = read()) !== null)\` empties the buffer per event regardless of how many chunks it holds, and asserting on 'end' waits until the readable side is fully done. Both align with the documented stream contract and work on all Node versions. --- test/lib.Stringifier.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/test/lib.Stringifier.js b/test/lib.Stringifier.js index 132eab3..e0b02e7 100644 --- a/test/lib.Stringifier.js +++ b/test/lib.Stringifier.js @@ -68,16 +68,16 @@ describe('Lib', function () { function testStringifier(chunks, rows, done) { /* jshint validthis:true */ - var data = ''; - var stringifier = this || lib.createJSONStringifier(); + let data = ''; + const stringifier = this || lib.createJSONStringifier(); stringifier.on('error', function (err) { done(err); }).on('readable', function () { - var chunk = this.read(); - if (chunk) { + let chunk; + while ((chunk = this.read()) !== null) { data += chunk; } - }).on('finish', function () { + }).on('end', function () { JSON.parse(data).should.eql(rows); done(); }); From ee987955282168e6c5df539a36966710c31caf6d Mon Sep 17 00:00:00 2001 From: Ian McHardy Date: Thu, 9 Jul 2026 13:13:15 -0400 Subject: [PATCH 04/13] [TEST] lengthen db.Lifecycle.js test timeouts (#42) --- test/acceptance/db.Lifecycle.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/acceptance/db.Lifecycle.js b/test/acceptance/db.Lifecycle.js index 07216aa..f51a704 100644 --- a/test/acceptance/db.Lifecycle.js +++ b/test/acceptance/db.Lifecycle.js @@ -30,6 +30,7 @@ describe('db', function () { }); it('client.readyState is "connected" after successful connect', function (done) { + this.timeout(5000); const client = hdb.createClient(getOptions()); client.connect(function (err) { if (err) return done(err); @@ -42,6 +43,7 @@ describe('db', function () { }); it('client.readyState is "closed" after disconnect', function (done) { + this.timeout(5000); const client = hdb.createClient(getOptions()); client.connect(function (err) { if (err) return done(err); @@ -61,6 +63,7 @@ describe('db', function () { }); it('disconnect when already disconnected is safe', function (done) { + this.timeout(5000); const client = hdb.createClient(getOptions()); client.connect(function (err) { if (err) return done(err); @@ -192,6 +195,7 @@ describe('db', function () { }); it('end() terminates the connection; subsequent exec produces an error', function (done) { + this.timeout(5000); const client = hdb.createClient(getOptions()); client.connect(function (err) { if (err) return done(err); From 30133c3de85ece27073fb8b5ddc9f19f89fd2dec Mon Sep 17 00:00:00 2001 From: Linjun He Date: Thu, 9 Jul 2026 14:10:51 -0400 Subject: [PATCH 05/13] [TEST] raise REAL_VECTOR dynamic-length input-type-error test timeout to 3s - add this.timeout(3000) to the REAL_VECTOR (dynamic length) 'should raise input type error' test to accommodate accumulated latency from 6 invalid-input round-trips via async.each on far HANA cloud servers - leave other DataType tests at Mocha's 2s default --- test/acceptance/db.DataType.js | 1 + 1 file changed, 1 insertion(+) diff --git a/test/acceptance/db.DataType.js b/test/acceptance/db.DataType.js index d325f69..3e162d2 100644 --- a/test/acceptance/db.DataType.js +++ b/test/acceptance/db.DataType.js @@ -1540,6 +1540,7 @@ describe('db', function () { }); it('should raise input type error', function (done) { + this.timeout(3000); var invalidTestData = [ { value: 5, From f3fd7fa98f477a28a41c0874f1bcb4fcfc512abb Mon Sep 17 00:00:00 2001 From: Linjun He Date: Thu, 9 Jul 2026 13:31:40 -0400 Subject: [PATCH 06/13] [INTERNAL] remove obsolete test/mocha.opts - delete test/mocha.opts, which has been silently ignored since the repo upgraded to Mocha 8+ (mocha.opts was deprecated in v6, removed in v8; current version is 11) - --require should was already redundant: several test files (lib.Writer.js, lib.Reader.js, hdb.Client.js, util.bignum.js, rep.part.js, acceptance/db.Authentication.js) require('should') directly, and once any of them runs the Object.prototype mutation covers the whole process - --growl referenced growlnotify (dead since ~2016) and was removed from Mocha in v7; would be a hard error if the file were parsed --- test/mocha.opts | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 test/mocha.opts diff --git a/test/mocha.opts b/test/mocha.opts deleted file mode 100644 index 64066d6..0000000 --- a/test/mocha.opts +++ /dev/null @@ -1,2 +0,0 @@ ---require should ---growl \ No newline at end of file From d19a5a79b0a510d9cd1f0f3024244c1482f314db Mon Sep 17 00:00:00 2001 From: "hyperspace-portal[bot]" <131973+hyperspace-portal[bot]@users.noreply.github.wdf.sap.corp> Date: Tue, 21 Jul 2026 13:56:13 -0400 Subject: [PATCH 07/13] [Hyperspace CI/CD Setup] Add GitHub Actions integration-test.yaml (#37) Co-authored-by: Michal Majewski --- .github/workflows/integration-test.yml | 60 ++++++++++++++++++++++++++ .github/workflows/piper.yaml | 11 +++++ 2 files changed, 71 insertions(+) create mode 100644 .github/workflows/integration-test.yml create mode 100644 .github/workflows/piper.yaml diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml new file mode 100644 index 0000000..fab183d --- /dev/null +++ b/.github/workflows/integration-test.yml @@ -0,0 +1,60 @@ +name: Node-hdb Integration Test +run-name: "Integration Test — ${{ github.event.pull_request.title || github.ref_name }}" +on: + push: + branches: [main, 'rel/*'] + pull_request: + branches: [main, 'rel/*'] + workflow_dispatch: +concurrency: + group: integration-test + cancel-in-progress: false +jobs: + Node-hdb-Integration-Test: + runs-on: [self-hosted, solinas] + strategy: + matrix: + node-version: ['20', '22', '24', '26'] + fail-fast: false + max-parallel: 1 + permissions: + contents: write + id-token: write + steps: + - run: echo "🎉 The job was automatically triggered by a ${{ github.event_name }} event." + - run: echo "🐧 This job is now running on a ${{ runner.os }} SUGAR runner!" + - run: echo "🔎 The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}." + - name: Check out repository code + uses: actions/checkout@v6 + - name: Set up Node.js environment + uses: actions/setup-node@v6 + with: + node-version: ${{ matrix.node-version }} + - run: echo "💡 The ${{ github.repository }} repository has been cloned to the runner." + - name: Create test/db/config.json + run: | + cat > ${{ github.workspace }}/test/db/config.json << EOF + { + "host": "${{ secrets.HDB_HOST }}", + "port": ${{ secrets.HDB_PORT }}, + "user": "${{ secrets.HDB_USER }}", + "password": "${{ secrets.HDB_PASSWORD }}", + "proxyHostname": null, + "proxyPassword": null, + "proxyUserName": null + } + EOF + - run: echo "🖥️ The workflow is now ready to test your code on the runner." + - name: List files in the repository + run: | + echo "Workspace is ${{ github.workspace }}" + echo "Listing files in the repository:" + ls -l ${{ github.workspace }} + echo "Showing the contents of test/db/config.json:" + cat ${{ github.workspace }}/test/db/config.json + - run: npm install + - name: Run tests + run: | + echo "Running tests..." + make test + - run: echo "🍏 This job's status is ${{ job.status }}." \ No newline at end of file diff --git a/.github/workflows/piper.yaml b/.github/workflows/piper.yaml new file mode 100644 index 0000000..9be165a --- /dev/null +++ b/.github/workflows/piper.yaml @@ -0,0 +1,11 @@ +name: Piper +on: + workflow_dispatch: + +jobs: + piper: + uses: "project-piper/piper-pipeline-github/.github/workflows/sap-piper-workflow.yml@v1" + secrets: inherit + permissions: + contents: write + id-token: write From 9af452eb61ae10a177fb6690478be497f5c2409c Mon Sep 17 00:00:00 2001 From: Linjun He Date: Thu, 23 Jul 2026 13:34:15 -0400 Subject: [PATCH 08/13] [INFRA] add backport action for release branch cherry-picks (#45) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - add .github/workflows/backport.yml that uses korthout/backport-action to auto-cherry-pick merged PRs to rel/* branches when labeled with `backport rel/` - fires on pull_request_target (closed, labeled); creates a backport PR for each matching label; conflicts leave conflict markers in the PR for manual resolution - restricts label pattern to `^backport (rel/[^ ]+)$` so only rel/* branches can be targeted - pin actions/checkout to df4cb1c (v6.0.3) and korthout/backport-action to 2e830a1 (v4.6.0) to make the audited action code immutable — a moved tag on pull_request_target with write permissions would otherwise be a supply-chain foothold --- .github/workflows/backport.yml | 40 ++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 .github/workflows/backport.yml diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml new file mode 100644 index 0000000..a353354 --- /dev/null +++ b/.github/workflows/backport.yml @@ -0,0 +1,40 @@ +name: Backport labeled PRs + +# When a PR is merged with a label of the form `backport rel/`, +# cherry-pick the merged commit onto the target release branch and open +# a backport PR. +# +# Example: label `backport rel/2.29` on a PR against `main` will open a +# PR containing the cherry-picked commit against `rel/2.29`. + +on: + pull_request_target: + types: [closed, labeled] + +jobs: + backport: + # Skip early when this event can't produce a backport: the PR isn't + # merged, or none of its labels look like a backport label. The + # backport-action itself would filter these out later, but this + # avoids spinning up a runner in the first place. + if: > + github.event.pull_request.merged && + contains(toJSON(github.event.pull_request.labels.*.name), '"backport rel/') + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + # WARNING: this workflow runs on `pull_request_target` with write + # permissions. Do NOT check out the PR's head ref here — only the + # base repo. All PR-controlled inputs (title, body) are passed as + # action inputs, never as shell arguments. + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + + - name: Create backport PRs + uses: korthout/backport-action@2e830a1d0b8269505846ddd407a70876913ad1f8 # v4.6.0 + with: + label_pattern: '^backport (rel/[^ ]+)$' + pull_title: '${pull_title} [backport ${target_branch}]' + pull_description: | + Backport of #${pull_number} to `${target_branch}`. From 396aae63f780c58643fd993876b750912eff7d5f Mon Sep 17 00:00:00 2001 From: Linjun He Date: Thu, 23 Jul 2026 13:37:42 -0400 Subject: [PATCH 09/13] [INFRA] fix GHSA-3jxr-9vmj-r5cp and GHSA-52cp-r559-cp3m (#46) --- package-lock.json | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/package-lock.json b/package-lock.json index 1552cdb..9d0a72b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -248,9 +248,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", - "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "version": "1.1.16", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz", + "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==", "dev": true, "license": "MIT", "dependencies": { @@ -652,9 +652,9 @@ } }, "node_modules/glob/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -883,9 +883,9 @@ } }, "node_modules/js-yaml": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", - "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.0.tgz", + "integrity": "sha512-1td788aAnnZ5qs7V2QIRl1owjtYpbKt749Y3xauqQgwIIGF/xXWz1wMTEBx5O3LK3lXLVuqXPdPxj2BoFHaW9Q==", "dev": true, "funding": [ { @@ -1062,9 +1062,9 @@ } }, "node_modules/mocha/node_modules/brace-expansion": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", - "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz", + "integrity": "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==", "dev": true, "license": "MIT", "dependencies": { @@ -1527,9 +1527,9 @@ } }, "node_modules/test-exclude/node_modules/brace-expansion": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", - "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", + "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", "dev": true, "license": "MIT", "dependencies": { From e819f3d6dca56e882e04b34afaa90ebeac10c7c2 Mon Sep 17 00:00:00 2001 From: Linjun He Date: Thu, 23 Jul 2026 14:28:49 -0400 Subject: [PATCH 10/13] [INFRA] fix backport workflow stuck waiting for runner on internal GitHub - change runs-on from `ubuntu-latest` to `[self-hosted, solinas]`; SAP's internal GitHub has no GitHub-hosted runners, so `ubuntu-latest` jobs queue forever with no runner to pick them up - align with the SUGAR runner label already used by integration-test.yml --- .github/workflows/backport.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backport.yml b/.github/workflows/backport.yml index a353354..14ad52c 100644 --- a/.github/workflows/backport.yml +++ b/.github/workflows/backport.yml @@ -20,7 +20,7 @@ jobs: if: > github.event.pull_request.merged && contains(toJSON(github.event.pull_request.labels.*.name), '"backport rel/') - runs-on: ubuntu-latest + runs-on: [self-hosted, solinas] permissions: contents: write pull-requests: write From f7d2b654ebeae580af39b7ad20e017453f23a908 Mon Sep 17 00:00:00 2001 From: David Brandow Date: Wed, 5 Aug 2026 15:57:16 -0400 Subject: [PATCH 11/13] [FEATURE] Is valid (#32) * Update Client.js * Update Connection.js * Create ConnectOptionFlagSet1.js * Update ConnectOption.js * Update ConnectOptionType.js * Update MessageType.js * Update index.js * Update index.js * Update ConnectOptionFlagSet1.js * Update Connection.js * Update ConnectOptions.js * Update index.js * Update index.js * Update db.Lifecycle.js * Update Connection.js * Update PartKind.js * Update Connection.js * Update Connection.js --- lib/Client.js | 4 +++ lib/protocol/Connection.js | 23 ++++++++++++++++ lib/protocol/common/ConnectOption.js | 1 + lib/protocol/common/ConnectOptionFlagSet1.js | 18 ++++++++++++ lib/protocol/common/ConnectOptionType.js | 1 + lib/protocol/common/MessageType.js | 5 ++-- lib/protocol/common/PartKind.js | 3 +- lib/protocol/common/index.js | 1 + lib/protocol/data/index.js | 1 + lib/protocol/part/ConnectOptions.js | 1 + lib/protocol/request/index.js | 7 +++++ test/acceptance/db.Lifecycle.js | 29 ++++++++++++++++++++ 12 files changed, 91 insertions(+), 3 deletions(-) create mode 100644 lib/protocol/common/ConnectOptionFlagSet1.js diff --git a/lib/Client.js b/lib/Client.js index 9f9114b..90aaf34 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -259,6 +259,10 @@ Client.prototype.setClientInfo = function setClientInfo(key, val) { } }; +Client.prototype.isValid = function isValid(options, cb) { + this._connection.isValid(options, cb); +}; + Client.prototype._execute = function _execute(command, options, cb) { var result = this._createResult(this._connection, options); this._connection.executeDirect({ diff --git a/lib/protocol/Connection.js b/lib/protocol/Connection.js index e172586..111dc5c 100644 --- a/lib/protocol/Connection.js +++ b/lib/protocol/Connection.js @@ -493,6 +493,10 @@ Connection.prototype.connect = function connect(options, cb) { {name : common.ClientContextOption.CLIENT_APPLICATION_PROGRAM, value : this._clientInfo.getApplication()} ]); + this.connectOptions.setOptions([ + {name : common.ConnectOption.FLAG_SET1, + value : common.ConnectOptionFlagSet1.SUPPORT_CLIENT_PING} + ]); const compressionFlags = compressor.determineCompressionFlags(options['compress']); if(compressionFlags) { @@ -699,6 +703,25 @@ Connection.prototype._setClientInfo = function _setClientInfo(key, val) { this._clientInfo.setProperty(key, val); }; +Connection.prototype.isValid = function isValid(options, cb) { + if (!this._queue || + this.readyState !== 'connected') { + return cb(false); + } + if (this.connectOptions && ((this.connectOptions["flagSet1"] & common.ConnectOptionFlagSet1.SUPPORT_CLIENT_PING) != 0)) { + this.enqueue(request.isValid(options), function (err, reply) { + return cb(err ? false : true); + }); + } else { + options = util.extend({ + command: "SELECT 1 FROM DUMMY WHERE 1=0" + }, options); + this.executeDirect(options, function (err, reply) { + return cb(err ? false : true); + }); + } +}; + function ConnectionState() { this.sessionId = -1; this.packetCount = -1; diff --git a/lib/protocol/common/ConnectOption.js b/lib/protocol/common/ConnectOption.js index 780b99e..e679b8d 100644 --- a/lib/protocol/common/ConnectOption.js +++ b/lib/protocol/common/ConnectOption.js @@ -33,6 +33,7 @@ module.exports = { OS_USER: 32, FULL_VERSION_STRING: 44, COMPRESSION_LEVEL_AND_FLAGS: 49, + FLAG_SET1: 53, REDIRECTION_TYPE: 57, REDIRECTED_HOST: 58, REDIRECTED_PORT: 59, diff --git a/lib/protocol/common/ConnectOptionFlagSet1.js b/lib/protocol/common/ConnectOptionFlagSet1.js new file mode 100644 index 0000000..8ca6dd1 --- /dev/null +++ b/lib/protocol/common/ConnectOptionFlagSet1.js @@ -0,0 +1,18 @@ +// Copyright 2026 SAP AG. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http: //www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, +// either express or implied. See the License for the specific +// language governing permissions and limitations under the License. +'use strict'; + +module.exports = { + SUPPORT_CLIENT_PING: 0x02000000, +}; diff --git a/lib/protocol/common/ConnectOptionType.js b/lib/protocol/common/ConnectOptionType.js index 4b47052..2ebaa8d 100644 --- a/lib/protocol/common/ConnectOptionType.js +++ b/lib/protocol/common/ConnectOptionType.js @@ -35,6 +35,7 @@ ConnectOptionType[ConnectOption.IGNORE_UNKNOWN_PARTS] = TypeCode.BOOLEAN; ConnectOptionType[ConnectOption.DATA_FORMAT_VERSION2] = TypeCode.INT; ConnectOptionType[ConnectOption.OS_USER] = TypeCode.STRING; ConnectOptionType[ConnectOption.FULL_VERSION_STRING] = TypeCode.STRING; +ConnectOptionType[ConnectOption.FLAG_SET1] = TypeCode.INT; ConnectOptionType[ConnectOption.REDIRECTION_TYPE] = TypeCode.INT; ConnectOptionType[ConnectOption.REDIRECTED_HOST] = TypeCode.STRING; ConnectOptionType[ConnectOption.REDIRECTED_PORT] = TypeCode.INT; diff --git a/lib/protocol/common/MessageType.js b/lib/protocol/common/MessageType.js index d9caca3..0e4811d 100644 --- a/lib/protocol/common/MessageType.js +++ b/lib/protocol/common/MessageType.js @@ -37,5 +37,6 @@ module.exports = { FETCH_NEXT_ITAB: 79, INSERT_NEXT_ITAB: 80, BATCH_PREPARE: 81, - DB_CONNECT_INFO: 82 -}; \ No newline at end of file + DB_CONNECT_INFO: 82, + CLIENT_PING: 94 +}; diff --git a/lib/protocol/common/PartKind.js b/lib/protocol/common/PartKind.js index 25f95c7..ae49ff6 100644 --- a/lib/protocol/common/PartKind.js +++ b/lib/protocol/common/PartKind.js @@ -66,5 +66,6 @@ module.exports = { BATCH_EXECUTE: 63, */ TRANSACTION_FLAGS: 64, - DB_CONNECT_INFO: 67 + DB_CONNECT_INFO: 67, + CLIENT_PING: 82 }; diff --git a/lib/protocol/common/index.js b/lib/protocol/common/index.js index 8d2d986..f23df0c 100644 --- a/lib/protocol/common/index.js +++ b/lib/protocol/common/index.js @@ -20,6 +20,7 @@ exports.CommandOption = require('./CommandOption'); exports.CommitOption = require('./CommitOption'); exports.DbConnectInfoOption = require('./DbConnectInfoOption'); exports.ConnectOption = require('./ConnectOption'); +exports.ConnectOptionFlagSet1 = require('./ConnectOptionFlagSet1'); exports.ConnectOptionType = require('./ConnectOptionType'); exports.ClientContextOption = require('./ClientContextOption'); exports.DataFormatVersion = require('./DataFormatVersion'); diff --git a/lib/protocol/data/index.js b/lib/protocol/data/index.js index a3001a6..f356b1d 100644 --- a/lib/protocol/data/index.js +++ b/lib/protocol/data/index.js @@ -66,6 +66,7 @@ rw[PartKind.RESULT_SET_METADATA] = ResultSetMetadata; rw[PartKind.CLIENT_INFO] = TextList; rw[PartKind.TRANSACTION_FLAGS] = TransactionFlags; rw[PartKind.DB_CONNECT_INFO] = Options; +rw[PartKind.CLIENT_PING] = Options; for (var name in PartKind) { /* jshint forin: false */ diff --git a/lib/protocol/part/ConnectOptions.js b/lib/protocol/part/ConnectOptions.js index 5045a41..c3c1c2d 100644 --- a/lib/protocol/part/ConnectOptions.js +++ b/lib/protocol/part/ConnectOptions.js @@ -144,6 +144,7 @@ ConnectOptions.prototype.KEYS = [ common.ConnectOption.DATA_FORMAT_VERSION2, common.ConnectOption.OS_USER, common.ConnectOption.FULL_VERSION_STRING, + common.ConnectOption.FLAG_SET1, common.ConnectOption.REDIRECTION_TYPE, common.ConnectOption.REDIRECTED_HOST, common.ConnectOption.REDIRECTED_PORT, diff --git a/lib/protocol/request/index.js b/lib/protocol/request/index.js index aca7e3d..c147c55 100644 --- a/lib/protocol/request/index.js +++ b/lib/protocol/request/index.js @@ -40,6 +40,7 @@ exports.writeLob = writeLob; exports.commit = commit; exports.rollback = rollback; exports.dbConnectInfo = dbConnectInfo; +exports.isValid = isValid; function createSegment(type, options) { options = options || {}; @@ -207,3 +208,9 @@ function addCommitOptions(segment, options) { } return segment; } + +function isValid(options) { + var segment = createSegment(MessageType.CLIENT_PING, options); + segment.add(PartKind.CLIENT_PING, []); + return segment; +} diff --git a/test/acceptance/db.Lifecycle.js b/test/acceptance/db.Lifecycle.js index f51a704..49a2414 100644 --- a/test/acceptance/db.Lifecycle.js +++ b/test/acceptance/db.Lifecycle.js @@ -207,6 +207,35 @@ describe('db', function () { }); }); + it('isValid() returns true when the session is connected, false when the session is disconnected', function (done) { + this.timeout(5000); + const client = hdb.createClient(getOptions()); + client.connect(function (err) { + if (err) return done(err); + client.isValid({}, function (valid) { + valid.should.equal(true); + client.exec("SELECT CURRENT_CONNECTION FROM SYS.DUMMY", function (err, results) { + if (err) return done(err); + const adminClient = hdb.createClient(getOptions()); + adminClient.connect(function (err) { + if (err) return done(err); + adminClient.exec("ALTER SYSTEM DISCONNECT SESSION '" + results[0].CURRENT_CONNECTION + "'", function (err) { + if (err) return done(err); + client.isValid({}, function (valid) { + valid.should.equal(false); + adminClient.disconnect(function (err) { + adminClient.end(); + client.end(); + done(); + }); + }); + }); + }); + }); + }); + }); + }); + }); }); From 23aa0e60b2685448c63c0543978a0f9c786bab87 Mon Sep 17 00:00:00 2001 From: Linjun He Date: Fri, 14 Aug 2026 11:23:43 -0400 Subject: [PATCH 12/13] [FIX] keep extra space for potential client info updates (#52) * Keep space for potential client info updates * [FIX] keep space for potential client info update segments - fix getUpdatedPropertiesSize to compute exact wire size of the CLIENT_INFO part, accounting for per-field length indicators, 8-byte alignment, PART_HEADER_LENGTH, and useCesu8 encoding - return 0 when no properties are pending, removing the need for a message-type gate in getAvailableSize * [TEST] add acceptance test for LOB exec with pending client info - verify that setting client info between prepare and exec does not cause Packet size limit exceeded when writing a LOB stream --------- Co-authored-by: Bob den Os --- lib/protocol/ClientInfo.js | 19 ++++++++++++++++--- lib/protocol/Connection.js | 1 + test/acceptance/db.Lob.js | 28 ++++++++++++++++++++++++++++ test/lib.Connection.js | 11 +++++++++++ 4 files changed, 56 insertions(+), 3 deletions(-) diff --git a/lib/protocol/ClientInfo.js b/lib/protocol/ClientInfo.js index 22630a5..aa2de96 100644 --- a/lib/protocol/ClientInfo.js +++ b/lib/protocol/ClientInfo.js @@ -13,9 +13,10 @@ // language governing permissions and limitations under the License. 'use strict'; -var common = require('./common'); -var util = require('../util'); -var MessageType = common.MessageType; +const common = require('./common'); +const util = require('../util'); +const MessageType = common.MessageType; +const TextList = require('./data/TextList'); module.exports = ClientInfo; @@ -75,3 +76,15 @@ ClientInfo.prototype.getUpdatedProperties = function getUpdatedProperties() { return res; }; +ClientInfo.prototype.getUpdatedPropertiesSize = function getUpdatedPropertiesSize(useCesu8) { + const hasUpdatedProperties = Object.keys(this._updatedProperties).length > 0; + if (!hasUpdatedProperties) { + return 0; + } + const self = this; + const fields = Object.keys(this._updatedProperties).reduce(function (p, c) { + p.push(c, self._properties[c]); + return p; + }, []); + return common.PART_HEADER_LENGTH + util.alignLength(TextList.getByteLength(fields, useCesu8), 8); +}; diff --git a/lib/protocol/Connection.js b/lib/protocol/Connection.js index 111dc5c..1cd817c 100644 --- a/lib/protocol/Connection.js +++ b/lib/protocol/Connection.js @@ -331,6 +331,7 @@ Connection.prototype.getAvailableSize = function getAvailableSize(forLobs = fals if (this._statementContext) { availableSize -= this._statementContext.size; } + availableSize -= this._clientInfo.getUpdatedPropertiesSize(this.useCesu8); return availableSize; }; diff --git a/test/acceptance/db.Lob.js b/test/acceptance/db.Lob.js index e55798d..0db6728 100644 --- a/test/acceptance/db.Lob.js +++ b/test/acceptance/db.Lob.js @@ -309,6 +309,34 @@ describe('db', function () { testInsertReadableStream(transformStream, expected, done); }); + it('should insert from a stream when client info is set after prepare', function (done) { + // Regression test for https://github.com/SAP/node-hdb/pull/293 + // Setting client info between prepare and exec caused getAvailableSize to + // over-report available space, leading to Packet size limit exceeded. + const hdb = require('../../'); + const options = require('../db').getOptions({ packetSize: 65536 }); + const smallClient = hdb.createClient(options); + smallClient.connect(function (err) { + if (err) { return done(err); } + smallClient.exec('CREATE LOCAL TEMPORARY TABLE #lob_test (data BLOB)', function (err) { + if (err) { return done(err); } + smallClient.prepare('INSERT INTO #lob_test VALUES (?)', function (err, stmt) { + if (err) { return done(err); } + smallClient.setClientInfo('size', '64'); + const gen = async function* () { + for (let i = 0; i < 64; i++) yield Buffer.alloc(1024); + }; + stmt.exec([stream.Readable.from(gen(), { objectMode: false })], function (err) { + smallClient.exec('DROP TABLE #lob_test', function () { + smallClient.end(); + done(err); + }); + }); + }); + }); + }); + }); + }); }); diff --git a/test/lib.Connection.js b/test/lib.Connection.js index acd74a4..6ec8ecf 100644 --- a/test/lib.Connection.js +++ b/test/lib.Connection.js @@ -413,6 +413,17 @@ describe('Lib', function () { var c8 = createConnection({ packetSize: ps, packetSizeLimit: psl }); c8.getAvailableSize(false).should.equal(packetSizeMax - totalHeaderLength); c8.getAvailableSize(true).should.equal(packetSizeMax - totalHeaderLength); + + // client info property set: LOCALE(6 bytes) + en_US(5 bytes), each +1 indicator = 13 data bytes, + // aligned to 8 = 16, plus PART_HEADER_LENGTH(16) = 32 total + var c9 = createConnection(); + c9._statementContext = { + size: 32, + }; + c9.getClientInfo().setProperty('LOCALE', 'en_US'); + var clientInfoPartSize = 32; + c9.getAvailableSize().should.equal(packetSizeDefault - totalHeaderLength - 32 - clientInfoPartSize); + c9.getAvailableSize(true).should.equal(packetSizeDefault - totalHeaderLength - 32 - clientInfoPartSize); }); it('should parse a reply', function () { From ebb310d1c1e4a5c06888f6a7a71f1c15cf3ab464 Mon Sep 17 00:00:00 2001 From: Linjun He Date: Fri, 14 Aug 2026 12:30:41 -0400 Subject: [PATCH 13/13] [RELEASE] bump up version to 2.29.6 --- cfg/VERSION | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/cfg/VERSION b/cfg/VERSION index 56c92d5..afd9243 100644 --- a/cfg/VERSION +++ b/cfg/VERSION @@ -1 +1 @@ -2.29.5 +2.29.6 diff --git a/package-lock.json b/package-lock.json index 9d0a72b..6a1d352 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "hdb", - "version": "2.29.5", + "version": "2.29.6", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "hdb", - "version": "2.29.5", + "version": "2.29.6", "license": "Apache-2.0", "dependencies": { "iconv-lite": "0.7.0" diff --git a/package.json b/package.json index ef867e1..664bc9d 100644 --- a/package.json +++ b/package.json @@ -13,7 +13,7 @@ ], "name": "hdb", "description": "SAP HANA Database Client for Node", - "version": "2.29.5", + "version": "2.29.6", "repository": { "type": "git", "url": "git://github.com/SAP/node-hdb.git"