Skip to content

Commit b28a058

Browse files
author
Georg Traar
committed
Add rowmode and changed default bigint serialization to number
1 parent 2c5b7d1 commit b28a058

File tree

12 files changed

+307
-57
lines changed

12 files changed

+307
-57
lines changed

.prettierrc

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,12 @@
22
"semi": true,
33
"singleQuote": true,
44
"trailingComma": "es5",
5-
"printWidth": 120
5+
"printWidth": 120,
6+
"tabWidth": 2,
7+
"useTabs": false,
8+
"bracketSpacing": true,
9+
"arrowParens": "always",
10+
"endOfLine": "lf",
11+
"quoteProps": "as-needed",
12+
"proseWrap": "preserve"
613
}

README.md

Lines changed: 40 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -76,18 +76,21 @@ The `CrateDBClient` can be configured with either environment variables or direc
7676

7777
#### Configuration Options
7878

79-
| Option | Type | Default Value | Description |
80-
| ------------------ | ----------------------- | ---------------------------------------------- | ------------------------------------------------------------ |
81-
| `user` | `string` | `'crate'` or `process.env.CRATEDB_USER` | Database user. |
82-
| `password` | `string` or `null` | `''` or `process.env.CRATEDB_PASSWORD` | Database password. |
83-
| `host` | `string` | `'localhost'` or `process.env.CRATEDB_HOST` | Database host. |
84-
| `port` | `number` | `4200` or `process.env.CRATEDB_PORT` | Database port. |
85-
| `defaultSchema` | `string` | `null` or `process.env.CRATEDB_DEFAULT_SCHEMA` | Default schema for queries. |
86-
| `connectionString` | `string` | `null` | Connection string, e.g., `https://user:password@host:port/`. |
87-
| `ssl` | `object` or `null` | `null` | SSL configuration; |
88-
| `keepAlive` | `boolean` | `true` | Enables HTTP keep-alive for persistent connections. |
89-
| `maxConnections` | `number` | `20` | Limits the maximum number of concurrent connections. |
90-
| `deserialization` | `DeserializationConfig` | see deserialization section | Controls deserialization behaviour |
79+
| Option | Type | Default Value | Description |
80+
| ---------- | ------------------ | --------------------------------------- | ------------------------------------ |
81+
| `user` | `string` | `'crate'` or `process.env.CRATEDB_USER` | Database user. |
82+
| `password` | `string` or `null` | `''` or `process.env.CRATEDB_PASSWORD` | Database password. |
83+
| `jwt` | `string \| null` | `null` | JWT token for Bearer authentication. |
84+
85+
| `host` | `string` | `'localhost'` or `process.env.CRATEDB_HOST` | Database host. |
86+
| `port` | `number` | `4200` or `process.env.CRATEDB_PORT` | Database port. |
87+
| `defaultSchema` | `string` | `null` or `process.env.CRATEDB_DEFAULT_SCHEMA` | Default schema for queries. |
88+
| `connectionString` | `string` | `null` | Connection string, e.g., `https://user:password@host:port/`. |
89+
| `ssl` | `object` or `null` | `null` | SSL configuration; |
90+
| `keepAlive` | `boolean` | `true` | Enables HTTP keep-alive for persistent connections. |
91+
| `maxConnections` | `number` | `20` | Limits the maximum number of concurrent connections. |
92+
| `deserialization` | `DeserializationConfig` | `{ long: 'number', timestamp: 'date', date: 'date' }` | Controls deserialization behaviour |
93+
| `rowMode` | `'array' \| 'object'` | `'array'` | Controls the format of returned rows. |
9194

9295
#### Environment Variables
9396

@@ -107,13 +110,34 @@ export CRATEDB_DEFAULT_SCHEMA=doc
107110

108111
### General Operations
109112

110-
#### execute(sql, [args])
113+
#### execute(sql, args?, config?)
111114

112-
Execute a raw SQL query.
115+
Execute a SQL query with optional parameters and configuration.
113116

114-
```js
117+
```typescript
118+
// Basic query
115119
await client.execute('SELECT * FROM my_table';);
116-
await client.execute('SELECT ?;', ['Hello World!']);
120+
// Parameterized query
121+
await client.execute('SELECT FROM my_table WHERE id = ?', [123]);
122+
// Query with row mode configuration
123+
await client.execute('SELECT FROM my_table', undefined, { rowMode: 'object' });
124+
```
125+
126+
The `rowMode` configuration determines how rows are returned:
127+
128+
- `'array'` (default): Returns rows as arrays of values
129+
- `'object'`: Returns rows as objects with column names as keys
130+
131+
Example responses:
132+
133+
```typescript
134+
// Basic query
135+
const result = await client.execute('SELECT * FROM my_table');
136+
console.log(result.rows); // [[1, 'Alice', 30], [2, 'Bob', 25]]
137+
138+
// Query with row mode configuration
139+
const result = await client.execute('SELECT * FROM my_table', undefined, { rowMode: 'object' });
140+
console.log(result.rows); // [{id: 1, name: 'Alice', age: 30}, {id: 2, name: 'Bob', age: 25}]
117141
```
118142

119143
#### executeMany(sql, bulk_args)

eslint.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,4 +23,4 @@ export default [
2323
// (The "plugin:prettier/recommended" config normally does this.)
2424
},
2525
},
26-
];
26+
];

examples/test.js

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import http from 'http';
2+
import zlib from 'zlib';
3+
4+
const requestData = JSON.stringify({
5+
stmt: 'SELECT * FROM sys.summits',
6+
});
7+
8+
// Compress request data using gzip
9+
zlib.deflate(requestData, (err, compressedData) => {
10+
if (err) {
11+
console.error('Compression failed:', err);
12+
return;
13+
}
14+
15+
const options = {
16+
hostname: 'localhost',
17+
port: 4200,
18+
path: '/_sql',
19+
method: 'POST',
20+
headers: {
21+
'Content-Type': 'application/json',
22+
'Content-Encoding': 'deflate', // Tell CrateDB the request is compressed
23+
Accept: 'application/json',
24+
'Accept-Encoding': 'gzip',
25+
},
26+
};
27+
28+
const req = http.request(options, (res) => {
29+
let responseData = '';
30+
31+
res.on('data', (chunk) => {
32+
responseData += chunk;
33+
});
34+
35+
res.on('end', () => {
36+
console.log('Response:', responseData);
37+
});
38+
});
39+
40+
req.on('error', (error) => {
41+
console.error('Request error:', error);
42+
});
43+
44+
req.write(compressedData); // Send gzipped request body
45+
req.end();
46+
});

package.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
"lint": "eslint 'src/**/*.{ts,tsx}' 'tests/**/*.{ts,tsx}'",
2626
"test": "vitest",
2727
"test:watch": "vitest --watch",
28-
"ci": "npm run check-format && npm run lint && npm test && npm run build"
28+
"ci": "npm run check-format && npm run lint && npm test && npm run build",
29+
"release": "npm run ci && npm version",
30+
"postrelease": "git push && git push --tags && npm publish"
2931
},
3032
"repository": {
3133
"type": "git",
@@ -63,4 +65,4 @@
6365
"engines": {
6466
"node": ">=21.0.0"
6567
}
66-
}
68+
}

src/CrateDBClient.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
OptimizeOptions,
1515
ColumnDefinition,
1616
TableOptions,
17+
QueryConfig,
1718
} from './interfaces';
1819
import { CrateDBError, DeserializationError, RequestError } from './utils/Error.js';
1920
import { StatementGenerator } from './StatementGenerator.js';
@@ -25,16 +26,17 @@ const defaultConfig: CrateDBConfig = {
2526
jwt: null, // JWT token for Bearer authentication
2627
host: process.env.CRATEDB_HOST || 'localhost',
2728
port: process.env.CRATEDB_PORT ? parseInt(process.env.CRATEDB_PORT, 10) : 4200, // Default CrateDB port
28-
defaultSchema: process.env.CRATEDB_DEFAULT_SCHEMA || null, // Default schema for queries
2929
connectionString: null,
3030
ssl: false,
31+
defaultSchema: process.env.CRATEDB_DEFAULT_SCHEMA || null, // Default schema for queries
3132
keepAlive: true, // Enable persistent connections by default
3233
maxConnections: 20,
3334
deserialization: {
34-
long: 'bigint',
35+
long: 'number',
3536
timestamp: 'date',
3637
date: 'date',
3738
},
39+
rowMode: 'array',
3840
};
3941

4042
export class CrateDBClient {
@@ -110,7 +112,7 @@ export class CrateDBClient {
110112
}
111113
}
112114

113-
async execute(stmt: string, args?: unknown[]): Promise<CrateDBResponse> {
115+
async execute(stmt: string, args?: unknown[], config?: QueryConfig): Promise<CrateDBResponse> {
114116
const startRequestTime = Date.now();
115117
const payload = args ? { stmt, args } : { stmt };
116118
let body: string;
@@ -121,11 +123,16 @@ export class CrateDBClient {
121123
throw new RequestError(`Serialization failed: ${msg}`);
122124
}
123125

124-
const options = { ...this.httpOptions, body };
126+
const options = {
127+
...this.httpOptions,
128+
...config?.httpOptions,
129+
body,
130+
};
125131

126132
try {
127133
const response = await this._makeRequest(options);
128-
return this._addDurations(startRequestTime, response) as CrateDBResponse;
134+
const transformedResponse = this._transformResponse(response, config?.rowMode ?? this.cfg.rowMode);
135+
return this._addDurations(startRequestTime, transformedResponse) as CrateDBResponse;
129136
} catch (error: unknown) {
130137
if (error instanceof CrateDBError || error instanceof DeserializationError) {
131138
throw error;
@@ -231,7 +238,7 @@ export class CrateDBClient {
231238
}
232239

233240
async update(tableName: string, options: Record<string, unknown>, whereClause: string): Promise<CrateDBResponse> {
234-
const { keys, values, args } = this._prepareOptions(options);
241+
const { args } = this._prepareOptions(options);
235242
const query = StatementGenerator.update(tableName, options, whereClause);
236243
return this.execute(query, args);
237244
}
@@ -362,6 +369,41 @@ export class CrateDBClient {
362369
});
363370
}
364371

372+
protected _transformResponse(
373+
response: CrateDBBaseResponse,
374+
rowMode: 'array' | 'object' = 'object'
375+
): CrateDBBaseResponse {
376+
// Return early if not transforming to object mode
377+
if (rowMode !== 'object') {
378+
return response;
379+
}
380+
381+
// Create a shallow copy of the response
382+
const transformedResponse = { ...response };
383+
384+
// Only transform if we have both rows and column names
385+
if (Array.isArray(transformedResponse.rows) && Array.isArray(transformedResponse.cols)) {
386+
transformedResponse.rows = transformedResponse.rows.map((row) => {
387+
// Skip transformation if row is null or not an array
388+
if (!Array.isArray(row)) {
389+
return row;
390+
}
391+
392+
const obj: Record<string, unknown> = {};
393+
transformedResponse.cols?.forEach((col, index) => {
394+
// Only set property if column name is a string
395+
if (typeof col === 'string') {
396+
// Preserve null/undefined values
397+
obj[col] = row[index];
398+
}
399+
});
400+
return obj;
401+
});
402+
}
403+
404+
return transformedResponse;
405+
}
406+
365407
public getConfig(): Readonly<CrateDBConfig> {
366408
return this.cfg;
367409
}

src/Cursor.ts

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,34 @@
33
import http from 'http';
44
import https from 'https';
55
import { CrateDBClient } from './CrateDBClient.js';
6-
import { CrateDBResponse, CrateDBRecord } from './interfaces.js';
7-
import { Serializer } from './Serializer.js';
6+
import { CrateDBRecord } from './interfaces.js';
7+
import { CrateDBError, RequestError } from './utils/Error.js';
88

99
export class Cursor {
1010
public client: CrateDBClient;
1111
public sql: string;
1212
public cursorName: string;
1313
public isOpen: boolean;
1414
public agent: http.Agent | https.Agent;
15-
public connectionOptions: http.RequestOptions;
15+
private cursorOptions: http.RequestOptions;
1616

1717
constructor(client: CrateDBClient, sql: string) {
18-
this.client = client; // Reference to the CrateDBClient instance
19-
this.sql = sql; // The SQL statement for the cursor
20-
this.cursorName = `cursor_${Date.now()}`; // Unique cursor name
21-
this.isOpen = false; // Cursor state
18+
this.client = client;
19+
this.sql = sql;
20+
this.cursorName = `cursor_${Date.now()}`;
21+
this.isOpen = false;
2222

2323
const agentOptions = {
2424
keepAlive: true,
2525
maxSockets: 1,
2626
};
2727

28-
// Create a new agent with its own socket for this cursor
2928
this.agent = client.getConfig().ssl ? new https.Agent(agentOptions) : new http.Agent(agentOptions);
30-
this.connectionOptions = { ...client.getHttpOptions(), agent: this.agent };
29+
30+
this.cursorOptions = {
31+
...client.getHttpOptions(),
32+
agent: this.agent,
33+
};
3134
}
3235

3336
async open(): Promise<void> {
@@ -87,16 +90,24 @@ export class Cursor {
8790
}
8891

8992
async _execute(sql: string): Promise<Array<CrateDBRecord>> {
90-
const options = { ...this.connectionOptions, body: Serializer.serialize({ stmt: sql }) };
9193
try {
92-
const response: CrateDBResponse = (await this.client._makeRequest(options)) as CrateDBResponse;
93-
const { cols, rows, rowcount } = response;
94-
return rowcount && rowcount > 0 ? this._rebuildObjects(cols || [], rows as unknown[]) : [];
95-
} catch (error: unknown) {
96-
if (error instanceof Error) {
97-
throw new Error(`Error executing SQL: ${sql}. Details: ${error.message}`);
94+
const response = await this.client.execute(sql, undefined, {
95+
rowMode: 'object',
96+
httpOptions: this.cursorOptions,
97+
});
98+
99+
if (!response.rows || !response.rowcount) {
100+
return [];
101+
}
102+
103+
return response.rows as unknown as Array<Record<string, unknown>>;
104+
} catch (error) {
105+
if (error instanceof CrateDBError) {
106+
throw error;
107+
} else if (error instanceof Error) {
108+
throw new RequestError(`Error executing SQL: ${sql}. Details: ${error.message}`, { cause: error });
98109
}
99-
throw error;
110+
throw new RequestError('CrateDB request failed with an unknown error');
100111
}
101112
}
102113

src/interfaces.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import type { RequestOptions } from 'http';
2+
13
export interface CrateDBConfig {
24
user: string;
35
password: string;
@@ -10,6 +12,7 @@ export interface CrateDBConfig {
1012
keepAlive: boolean;
1113
maxConnections: number;
1214
deserialization: DeserializationConfig;
15+
rowMode?: 'array' | 'object';
1316
}
1417

1518
export type DeserializationConfig = {
@@ -18,11 +21,17 @@ export type DeserializationConfig = {
1821
date: 'date' | 'number';
1922
};
2023

24+
export type QueryConfig = {
25+
rowMode?: 'array' | 'object';
26+
httpOptions?: RequestOptions;
27+
};
28+
2129
export interface CrateDBBaseResponse {
30+
rows?: unknown[];
2231
cols?: string[];
2332
col_types?: number[];
24-
rows?: Array<Array<unknown>>;
2533
duration?: number;
34+
rowcount?: number;
2635
durations: {
2736
cratedb?: number;
2837
request: number;

0 commit comments

Comments
 (0)