Skip to content

Commit 50f9170

Browse files
author
Georg Traar
committed
Feat: (De)serialization of bigint and long
1 parent 5530196 commit 50f9170

File tree

12 files changed

+212
-60
lines changed

12 files changed

+212
-60
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: Set up Node.js
1919
uses: actions/setup-node@v4
2020
with:
21-
node-version: 18
21+
node-version: 24
2222
registry-url: https://registry.npmjs.org/
2323

2424
- name: Install dependencies

.github/workflows/test.yml

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,18 @@ jobs:
1212
test:
1313
runs-on: ubuntu-latest
1414

15+
strategy:
16+
matrix:
17+
node-version: ['20.x', '22.x']
18+
1519
steps:
1620
- name: Checkout code
17-
uses: actions/checkout@v3
21+
uses: actions/checkout@v4
1822

19-
- name: Set up Node.js
23+
- name: Use Node.js ${{ matrix.node-version }}
2024
uses: actions/setup-node@v4
2125
with:
22-
node-version: 18
26+
node-version: ${{ matrix.node-version }}
2327

2428
- name: Install dependencies
2529
run: npm install

README.md

Lines changed: 50 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55

66
This library is a lightweight Node.js client derived from `node-crate` for interacting with CrateDB via its **HTTP endpoint**. Unlike libraries such as `node-postgres`, which use the PostgreSQL wire protocol, this client communicates with CrateDB's native HTTP API.
77

8-
> [!CAUTION]
9-
> **This library is primarily a proof of concept.**
10-
> While it provides basic functionality to interact with CrateDB, it is not production-ready and lacks the robustness of established libraries.
11-
>
8+
> [!CAUTION] > **This library is primarily a proof of concept.**
9+
> While it provides basic functionality to interact with CrateDB, it is not production-ready and lacks the robustness of established libraries.
10+
>
1211
> For production use, consider mature libraries like [`node-postgres`](https://node-postgres.com/) which leverage CrateDB's PostgreSQL compatibility. Use this client only for **testing, experimentation**, or if you know what you're doing. :wink:
1312
14-
1513
## Installation
1614

1715
To install `node-cratedb` using npm:
@@ -44,10 +42,10 @@ const client = new CrateDBClient({
4442
password: 'secretpassword!!',
4543
host: 'my.database-server.com',
4644
port: 4200,
47-
ssl: true, // Use HTTPS
48-
keepAlive: true, // Enable persistent connections
49-
maxConnections: 20, // Limit to 10 concurrent sockets
50-
defaultSchema: 'my_schema' // Default schema for queries
45+
ssl: true, // Use HTTPS
46+
keepAlive: true, // Enable persistent connections
47+
maxConnections: 20, // Limit to 10 concurrent sockets
48+
defaultSchema: 'my_schema', // Default schema for queries
5149
});
5250
```
5351

@@ -58,29 +56,28 @@ import { CrateDBClient } from '@proddata/node-cratedb';
5856

5957
const client = new CrateDBClient({
6058
host: 'my.database-server.com',
61-
jwt: 'your.jwt.token.here', // Use JWT for Bearer authentication
62-
ssl: true
59+
jwt: 'your.jwt.token.here', // Use JWT for Bearer authentication
60+
ssl: true,
6361
});
6462
```
6563

66-
6764
### Configuration
6865

6966
The `CrateDBClient` can be configured with either environment variables or directly with an options object. Below are the configuration options, along with their default values.
7067

7168
#### Configuration Options
7269

73-
| Option | Type | Default Value | Description |
74-
|--------------------|---------------------|-------------------------------------------------|-----------------------------------------------------------------|
75-
| `user` | `string` | `'crate'` or `process.env.CRATEDB_USER` | Database user. |
76-
| `password` | `string` or `null` | `''` or `process.env.CRATEDB_PASSWORD` | Database password. |
77-
| `host` | `string` | `'localhost'` or `process.env.CRATEDB_HOST` | Database host. |
78-
| `port` | `number` | `4200` or `process.env.CRATEDB_PORT` | Database port. |
79-
| `defaultSchema` | `string` | `null` or `process.env.CRATEDB_DEFAULT_SCHEMA` | Default schema for queries. |
80-
| `connectionString` | `string` | `null` | Connection string, e.g., `https://user:password@host:port/`. |
81-
| `ssl` | `object` or `null` | `null` | SSL configuration; |
82-
| `keepAlive` | `boolean` | `true` | Enables HTTP keep-alive for persistent connections. |
83-
| `maxConnections` | `number` | `20` | Limits the maximum number of concurrent connections. |
70+
| Option | Type | Default Value | Description |
71+
| ------------------ | ------------------ | ---------------------------------------------- | ------------------------------------------------------------ |
72+
| `user` | `string` | `'crate'` or `process.env.CRATEDB_USER` | Database user. |
73+
| `password` | `string` or `null` | `''` or `process.env.CRATEDB_PASSWORD` | Database password. |
74+
| `host` | `string` | `'localhost'` or `process.env.CRATEDB_HOST` | Database host. |
75+
| `port` | `number` | `4200` or `process.env.CRATEDB_PORT` | Database port. |
76+
| `defaultSchema` | `string` | `null` or `process.env.CRATEDB_DEFAULT_SCHEMA` | Default schema for queries. |
77+
| `connectionString` | `string` | `null` | Connection string, e.g., `https://user:password@host:port/`. |
78+
| `ssl` | `object` or `null` | `null` | SSL configuration; |
79+
| `keepAlive` | `boolean` | `true` | Enables HTTP keep-alive for persistent connections. |
80+
| `maxConnections` | `number` | `20` | Limits the maximum number of concurrent connections. |
8481

8582
#### Environment Variables
8683

@@ -114,10 +111,9 @@ await client.execute('SELECT ?;', ['Hello World!']);
114111
Execute a raw bulk SQL query.
115112

116113
```js
117-
await client.execute('SELECT ?;', [['Hello'],['World']]);
114+
await client.execute('SELECT ?;', [['Hello'], ['World']]);
118115
```
119116

120-
121117
#### streamQuery(sql, batchSize)
122118

123119
The `streamQuery` method in CrateDBClient wraps the Cursor functionality
@@ -147,10 +143,10 @@ If `primaryKeys` are provided, the method will handle conflicts by updating the
147143

148144
```javascript
149145
// Insert a row with primary key conflict resolution
150-
await client.insert("my_table", { id: 1, column1: "value1", column2: "value2" }, ["id"]);
146+
await client.insert('my_table', { id: 1, column1: 'value1', column2: 'value2' }, ['id']);
151147

152148
// Insert a row without conflict resolution
153-
await client.insert("my_table", { id: 1, column1: "value1", column2: "value2" });
149+
await client.insert('my_table', { id: 1, column1: 'value1', column2: 'value2' });
154150
```
155151

156152
#### insertMany(tableName, jsonArray, primaryKeys = null)
@@ -165,15 +161,15 @@ If `primaryKeys` are provided, the method will handle conflicts by updating the
165161

166162
```javascript
167163
const bulkData = [
168-
{ id: 1, name: "Earth", kind: "Planet", description: "A beautiful place." },
169-
{ id: 2, name: "Mars", kind: "Planet", description: "The red planet." },
170-
{ id: 1, name: "Earth Updated", kind: "Planet", description: "Updated description." }, // Conflict on id
164+
{ id: 1, name: 'Earth', kind: 'Planet', description: 'A beautiful place.' },
165+
{ id: 2, name: 'Mars', kind: 'Planet', description: 'The red planet.' },
166+
{ id: 1, name: 'Earth Updated', kind: 'Planet', description: 'Updated description.' }, // Conflict on id
171167
];
172168

173-
await client.insertMany("my_table", bulkData, ["id"]);
169+
await client.insertMany('my_table', bulkData, ['id']);
174170
// Conflicting row with `id: 1` will be updated instead of skipped.
175171

176-
await client.insertMany("my_table", bulkData);
172+
await client.insertMany('my_table', bulkData);
177173
// Conflicting rows will be skipped as no `primaryKeys` are provided.
178174
```
179175

@@ -209,7 +205,6 @@ Refresh a specified table.
209205
await client.refresh('my_table');
210206
```
211207

212-
213208
#### createTable(schema)
214209

215210
Create a new table based on a schema definition.
@@ -219,8 +214,8 @@ await client.createTable({
219214
my_table: {
220215
id: 'INTEGER PRIMARY KEY',
221216
name: 'STRING',
222-
created_at: 'TIMESTAMP'
223-
}
217+
created_at: 'TIMESTAMP',
218+
},
224219
});
225220
```
226221

@@ -257,8 +252,27 @@ for await (const row of cursor.iterate(5)) {
257252
await cursor.close();
258253
```
259254

255+
## Handling JavaScript `BigInt` and CrateDB `LONG` Values
256+
257+
This library leverages modern JavaScript features — such as `JSON.rawJSON()` -
258+
to accurately serialize and deserialize `BigInt` values.
259+
260+
### Serialization
261+
262+
- **BigInt to LONG:**
263+
When serializing, JavaScript `BigInt` values their precision is preserved.
264+
e.g. `BigInt(12345678901234567890)` is serialized as `12345678901234567890`.
265+
266+
### Deserialization
267+
268+
- **Top-Level LONG Columns:**
269+
If type information is available in the result set, columns defined as CrateDB
270+
`LONG` are automatically converted to `BigInt`.
271+
272+
- **Large Integer Values:**
273+
If type information is unavailable, integers exceeding `Number.MAX_SAFE_INTEGER`
274+
and without a decimal point are converted to `BigInt` on a best-effort basis.
260275

261276
## License
262277

263278
MIT License. Feel free to use and modify this library as per the terms of the license.
264-

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,6 @@
4949
"vitest": "^2.1.5"
5050
},
5151
"engines": {
52-
"node": ">=16.0.0"
52+
"node": ">=21.0.0"
5353
}
54-
}
54+
}

src/CrateDBClient.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import http, { AgentOptions } from 'http';
33
import https from 'https';
44
import { URL } from 'url';
55
import { CrateDBCursor } from './CrateDBCursor.js';
6+
import { CrateDBSerializer } from '../src/CrateDBSerializer';
67
import {
78
CrateDBConfig,
89
CrateDBBaseResponse,
@@ -120,7 +121,7 @@ export class CrateDBClient {
120121
bulk_args: unknown[][] | null = null
121122
): Promise<CrateDBBaseResponse> {
122123
const startRequestTime = Date.now();
123-
const body = JSON.stringify(args ? { stmt, args } : { stmt, bulk_args });
124+
const body = CrateDBSerializer.stringify(args ? { stmt, args } : { stmt, bulk_args });
124125
const options = { ...this.httpOptions, body };
125126
const response = await this._makeRequest(options);
126127
const totalRequestTime = Date.now() - startRequestTime;
@@ -271,7 +272,7 @@ export class CrateDBClient {
271272
const rawResponse = Buffer.concat(data); // Raw response data as a buffer
272273
const responseBodySize = rawResponse.length;
273274
try {
274-
const parsedResponse = JSON.parse(rawResponse.toString());
275+
const parsedResponse = CrateDBSerializer.deserialize(rawResponse.toString());
275276
resolve({
276277
...parsedResponse,
277278
sizes: { response: responseBodySize, request: requestBodySize },

src/CrateDBCursor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import http from 'http';
44
import https from 'https';
55
import { CrateDBClient } from './CrateDBClient.js';
66
import { CrateDBResponse, CrateDBRecord } from './interfaces';
7+
import { CrateDBSerializer } from './CrateDBSerializer.js';
78

89
export class CrateDBCursor {
910
public client: CrateDBClient;
@@ -86,7 +87,7 @@ export class CrateDBCursor {
8687
}
8788

8889
async _execute(sql: string): Promise<Array<CrateDBRecord>> {
89-
const options = { ...this.connectionOptions, body: JSON.stringify({ stmt: sql }) };
90+
const options = { ...this.connectionOptions, body: CrateDBSerializer.stringify({ stmt: sql }) };
9091
try {
9192
const response: CrateDBResponse = await this.client._makeRequest(options);
9293
const { cols, rows, rowcount } = response;

src/CrateDBSerializer.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { CrateDBTypes } from './CrateDBTypes';
2+
import { CrateDBBaseResponse } from './interfaces';
3+
4+
export class CrateDBSerializer {
5+
static stringify(obj: unknown): string {
6+
return JSON.stringify(obj, replacer);
7+
}
8+
9+
static parse(str: string): CrateDBBaseResponse {
10+
return JSON.parse(str, reviver);
11+
}
12+
13+
static deserialize(str: string) {
14+
const obj = this.parse(str);
15+
obj.col_types?.forEach((type: number, index: number) => {
16+
switch (type) {
17+
case CrateDBTypes.BIGINT:
18+
obj.rows?.forEach((row: Array<unknown>) => {
19+
if (typeof row[index] === 'number') {
20+
row[index] = BigInt(String(row[index]));
21+
}
22+
});
23+
break;
24+
default:
25+
}
26+
});
27+
return obj;
28+
}
29+
}
30+
31+
function replacer(_: unknown, value: unknown) {
32+
if (typeof value === 'bigint') {
33+
return JSON.rawJSON(value);
34+
}
35+
return value;
36+
}
37+
38+
type Context = {
39+
source: string;
40+
};
41+
42+
function reviver(_: unknown, value: unknown, context: Context | null = null): unknown {
43+
//if number is greater than Number.MAX_SAFE_INTEGER and not a float
44+
if (
45+
typeof value === 'number' &&
46+
value > Number.MAX_SAFE_INTEGER &&
47+
context !== null &&
48+
!context.source.includes('.')
49+
) {
50+
return BigInt(context.source);
51+
}
52+
return value;
53+
}

src/CrateDBTypes.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
export enum CrateDBTypes {
2+
NULL = 0,
3+
NOT_SUPPORTED = 1,
4+
CHAR = 2,
5+
BOOLEAN = 3,
6+
TEXT = 4,
7+
IP = 5,
8+
DOUBLE_PRECISION = 6,
9+
REAL = 7,
10+
SMALLINT = 8,
11+
INTEGER = 9,
12+
BIGINT = 10,
13+
TIMESTAMP_WITH_TIME_ZONE = 11,
14+
OBJECT = 12,
15+
GEO_POINT = 13,
16+
GEO_SHAPE = 14,
17+
TIMESTAMP_WITHOUT_TIME_ZONE = 15,
18+
UNCHECKED_OBJECT = 16,
19+
INTERVAL = 17,
20+
REGPROC = 19,
21+
TIME = 20,
22+
OIDVECTOR = 21,
23+
NUMERIC = 22,
24+
REGCLASS = 23,
25+
DATE = 24,
26+
BIT = 25,
27+
JSON = 26,
28+
CHARACTER = 27,
29+
FLOAT_VECTOR = 28,
30+
ARRAY = 100,
31+
}

src/interfaces.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export interface CrateDBConfig {
1313

1414
export interface CrateDBBaseResponse {
1515
cols?: string[];
16+
col_types?: number[];
17+
rows?: Array<Array<unknown>>;
1618
duration?: number;
1719
durations: {
1820
cratedb?: number;
@@ -27,7 +29,6 @@ export interface CrateDBBaseResponse {
2729
}
2830

2931
export interface CrateDBResponse extends CrateDBBaseResponse {
30-
col_types?: number[];
3132
rows?: Array<Array<unknown>>;
3233
rowcount?: number;
3334
}

src/json-extensions.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// Extend the JSON interface to include rawJSON, available in Node.js v21.0.0+.
2+
// More info: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/rawJSON
3+
declare global {
4+
interface JSON {
5+
rawJSON(value: unknown): unknown;
6+
}
7+
}
8+
export {};

0 commit comments

Comments
 (0)