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:
// 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 numberRoot 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:
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
| Encoding | Range | Marker | Bytes | JS Decoding |
|---|---|---|---|---|
| Tiny | -16 to 127 | inline (0xF0-0x7F) | 1 | Number |
| INT8 | -128 to -17 | 0xC8 | 2 | Number |
| INT16 | -32768 to 32767 | 0xC9 | 3 | Number |
| INT32 | -2147483648 to 2147483647 | 0xCA | 5 | Number |
| INT64 | < INT32 min or > INT32 max | 0xCB | 9 | BigInt |
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)
// usedCount values: 0, 1, 2, ..., 100
encodePackStreamInt(1) → [0x01] (1 byte, tiny) → JS Number ✅
encodePackStreamInt(100) → [0x64] (1 byte, tiny) → JS Number ✅Larger Values
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)
encodePackStreamInt(2147483648) → [0xCB, 0x00, 0x00, ...] (INT64) → JS BigInt ⚠️
// This will cause issues in JavaScript arithmetic!Testing
Run JavaScript compatibility tests:
go test ./pkg/bolt -run TestJavaScript -vTests 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
| Language | INT32 & Smaller | INT64 | Issues? |
|---|---|---|---|
| JavaScript | Number | BigInt | Cannot mix types |
| Python | int | int | No issues |
| Go | int64 | int64 | No issues |
| Java | Integer/Long | Long | Autoboxing works |
References
Related Issues
- Reported issue:
usedCount + 1causing BigInt/Number mixing errors - Fix: Already using optimal encoding; added documentation
- Tests:
pkg/bolt/javascript_compat_test.go