Repository navigation
fix: send LUW LOB parameters beyond 32767 bytes as EXTDTA - #32
Merged
Merged
Conversation
Parameters bound to CLOB/BLOB/DBCLOB targets declared larger than 32767 bytes were described in SQLDTA as VARCHAR/VARBINARY/VARGRAPHIC with lengths such as 0x8000 or 0xFFFF. An FD:OCA varying-length descriptor only has 15 bits of length, so LUW 11.5 and 12.1 answered with DSCINVRM (0x220A). The driver ignored that reply, so the statement reported rowCount=0 without an error and nothing was written (#31). LUW input descriptors for such LOBs now use DRDA LOB types with the same 9-byte reference LUW returns for LOB results: status 0x02 and an eight-byte big-endian length (UTF-16 code units for DBCLOB). Values follow SQLDTA as same-correlation EXTDTA objects in parameter order, each with the nullable indicator. Smaller LOB targets keep the existing encoding, and z/OS descriptors are unchanged. DSCINVRM is now classified as a protocol reply error, so rejected descriptors can no longer report success.
LUW pairs EXTDTA objects only with nonempty LOB placeholders. Sending an
indicator-only EXTDTA for an empty value shifted every later LOB value
and LUW rejected the request ("lob lengths don't match"). A zero-length
placeholder is now complete on its own.
XML inputs declare no length, so values beyond 32767 bytes could not fit
the VARCHAR descriptor and failed client-side. On LUW they are now sent
as LOBCMIXED placeholders with EXTDTA, which Db2 parses into the XML
target exactly like the VARCHAR form.
normalizeParam turned every Buffer, typed array and ArrayBuffer into a JS array, which the addon then read one element at a time and held as one JSON value per byte. With LOB parameters beyond 32 KB now written, a 50 MB BLOB took ~3 s and blocked the event loop for most of it. Byte views now reach the addon as Uint8Array, which copies them once into a Vec<u8>, and parameters move into Db2Value instead of being cloned. A 50 MB BLOB update drops from ~3.0 s to ~0.5 s, on par with CLOB.
gurungabit
marked this pull request as ready for review
October 5, 2026 01:37
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #31.
Root cause
When a parameter targets a
CLOB/BLOB/DBCLOBdeclared larger than 32,767 bytes, DESCRIBE INPUT reports its declared length (clamped tou16).build_sqldtathen described the parameter asVARCHAR/VARBINARY/VARGRAPHICwith that length, for example33 80 00forCLOB(32768)or29 FF FFforBLOB(1M). FD:OCA varying-length descriptors only have 15 bits of length. A set high bit declares a LOB placeholder instead, so Db2 LUW 11.5.9 and 12.1 rejected the SQLDTA with DSCINVRM (0x220A, invalid description).The driver did not classify
0x220A, soprocess_execute_replyskipped it. The chained commit then succeeded and the statement reportedrowCount=0with no error.CAST(? AS VARCHAR(100))worked because its descriptor stayed below 0x8000. Statements with more parameters likely raised an error on 1.0.24 because SQLDARD parsing rejected large LOB declarations there. 1.0.25 accepts them (404|408|412|988up to0x7FFF_FFFF), which exposed the encoding gap.Wire capture on 12.1 before the fix:
Fix
CFLOBCMIXED,C9BLOB,CDDBCLOB (nullable variants). Each uses the same 9-byte reference LUW returns for LOB result columns (0x8009): status0x02plus an eight-byte big-endian length. DBCLOB lengths count UTF-16 code units, matching LUW VARGRAPHIC input.SqlDta::write.How the wire format was found: a 4- or 8-byte placeholder produced
DTAMCHRM ... Parser: Data mismatchwithSUBCODE2=<our width>, SUBCODE3=9. Status bytes0x00/0x01were rejected. Omitting the EXTDTA indicator or using a DBCLOB byte length producedSYNTAXRM. UTF-16LE DBCLOB data stored the wrong characters.Binary parameter performance
The fix makes multi-megabyte BLOB parameters usable, which exposed a pre-existing cost.
normalizeParaminindex.cjsturned everyBuffer/typed array/ArrayBufferinto a JS array viaArray.from. The addon then read it one element at a time into one JSON value per byte, mostly on the event loop.Uint8Array. Other typed arrays andDataVieware re-viewed without copying. The addon copies the bytes once into aVec<u8>.Db2Valueinstead of being cloned.UPDATEdrops from ~3.0 s to ~0.5 s, on par with CLOB. A batch of 1,000 × 256 KB BLOBs goes from 20.4 s to under 3 s.Tests
connection.rs):DssReader.process_execute_reply.lob_regressions_test.rs):lob-regressions.test.ts):executeBatchwith NULL/empty, and the OPNQRY path with a LOB parameter.executeBatch, alternating direct/prepared updates, and a CALL with LOB IN parameters.executeBatchwith small/NULL/large,XMLEXISTS(... CAST(? AS XML)), and a malformed-document 2200M.Uint8Arrayviews,Int16Array,DataView,ArrayBuffer, plain byte arrays, an 8 MB Buffer, and invalid byte-array rejection.Validation
All run against local Docker Db2 LUW 12.1.0.0 and 11.5.9.0:
cargo fmt --check,cargo clippy --workspace --all-targets -D warnings, andcargo test --workspace --lib+-p db2-protoall pass. Thedb2-napiJS tests pass 17/17.cargo test --workspace --test '*'passes.npm test: 12.1 has 129 passed, 0 failed, 7 skipped (credential/TLS fixtures not supplied). 11.5 has 14 failures, all SQL0204 for the pre-existing missing EMPLOYEES/TEST_NULLS/TEST_STRINGS/ACCOUNTS fixtures.VALUES CAST(? AS CLOB/BLOB),INSERT … SELECT, andMERGEwith a CLOB parameter.examples/performancesuites on 12.1, branch vs.main): CRUD, batch, pipeline, blob, edge-case and pool numbers match within run-to-run noise, with no regressions. LOB parameter throughput at 50 MB is about 95 MB/s for CLOB and BLOB writes and about 100 MB/s for reads.Limits
🤖 Generated with Claude Code