Skip to content

PackStream Integer Encoding for JavaScript Compatibility ​

Problem ​

Client applications reported that integer values (like usedCount) were being returned as JavaScript BigInt instead of regular Number, causing runtime errors:

javascript
// ERROR: Cannot mix BigInt and other types
const usedCount = result.records[0].get("usedCount"); // BigInt from NornicDB
const nextCount = usedCount + 1; // ❌ TypeError: can't convert BigInt to number

Root Cause ​

The Neo4j JavaScript driver has specific behavior for decoding PackStream integers:

  • INT32 and smaller (markers 0xC8, 0xC9, 0xCA, or inline) → decoded as JavaScript Number
  • INT64 (marker 0xCB) → decoded as JavaScript BigInt

NornicDB was already using optimal encoding, but this documents the Neo4j compatibility requirements.

Solution ​

NornicDB's encodePackStreamInt function now explicitly documents Neo4j compatibility and uses the smallest possible encoding:

go
func encodePackStreamInt(val int64) []byte {
    // Tiny: -16 to 127 → 1 byte (inline)
    // INT8: -128 to -17 → 2 bytes (marker 0xC8)
    // INT16: -32768 to 32767 → 3 bytes (marker 0xC9)
    // INT32: -2147483648 to 2147483647 → 5 bytes (marker 0xCA)
    // INT64: everything else → 9 bytes (marker 0xCB)
}

PackStream Encoding Ranges ​

EncodingRangeMarkerBytesJS Decoding
Tiny-16 to 127inline (0xF0-0x7F)1Number
INT8-128 to -170xC82Number
INT16-32768 to 327670xC93Number
INT32-2147483648 to 21474836470xCA5Number
INT64< INT32 min or > INT32 max0xCB9BigInt

JavaScript Safe Integer Range ​

JavaScript's Number.MAX_SAFE_INTEGER is 2^53 - 1 (9,007,199,254,740,991), but PackStream INT32 max is only 2^31 - 1 (2,147,483,647).

Key Insight: Since INT32 range is entirely within JavaScript's safe integer range, using INT32 encoding (or smaller) guarantees the Neo4j driver will return a regular Number, not a BigInt.

Examples ​

usedCount (Typical Values) ​

go
// usedCount values: 0, 1, 2, ..., 100
encodePackStreamInt(1)   → [0x01]           (1 byte, tiny)    → JS Number ✅
encodePackStreamInt(100) → [0x64]           (1 byte, tiny)    → JS Number ✅

Larger Values ​

go
encodePackStreamInt(1000)       → [0xC9, 0x03, 0xE8]         (INT16) → JS Number ✅
encodePackStreamInt(100000)     → [0xCA, 0x00, 0x01, ...]   (INT32) → JS Number ✅
encodePackStreamInt(2147483647) → [0xCA, 0x7F, 0xFF, ...]   (INT32) → JS Number ✅

INT64 Boundary (Becomes BigInt) ​

go
encodePackStreamInt(2147483648) → [0xCB, 0x00, 0x00, ...] (INT64) → JS BigInt ⚠️
// This will cause issues in JavaScript arithmetic!

Testing ​

Run JavaScript compatibility tests:

bash
go test ./pkg/bolt -run TestJavaScript -v

Tests verify:

  • Small integers use tiny encoding (1 byte)
  • Medium integers use INT16/INT32 encoding
  • Only values outside INT32 range use INT64
  • The usedCount scenario works correctly

Neo4j Driver Behavior by Language ​

LanguageINT32 & SmallerINT64Issues?
JavaScriptNumberBigIntCannot mix types
PythonintintNo issues
Goint64int64No issues
JavaInteger/LongLongAutoboxing works

References ​

  • Reported issue: usedCount + 1 causing BigInt/Number mixing errors
  • Fix: Already using optimal encoding; added documentation
  • Tests: pkg/bolt/javascript_compat_test.go
Analytics privacy