monetdb 1.3.4 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/Linux.yml +3 -3
- package/.github/workflows/docs.yml +79 -0
- package/.github/workflows/macos.yml +3 -3
- package/.github/workflows/monetdb-versions.yml +43 -0
- package/README.md +41 -511
- package/docs/components/alert.tsx +10 -0
- package/docs/components/info.tsx +6 -0
- package/docs/next.config.js +24 -0
- package/docs/package-lock.json +5069 -0
- package/docs/package.json +22 -0
- package/docs/pages/_app.js +9 -0
- package/docs/pages/_meta.json +16 -0
- package/docs/pages/apis/_meta.json +4 -0
- package/docs/pages/apis/connection.mdx +60 -0
- package/docs/pages/apis/result.mdx +39 -0
- package/docs/pages/index.mdx +27 -0
- package/docs/theme.config.js +35 -0
- package/docs/v1/README.md +532 -0
- package/package.json +16 -20
- package/src/PrepareStatement.ts +37 -0
- package/src/connection.ts +125 -0
- package/src/defaults.ts +13 -0
- package/src/file-transfer.ts +173 -0
- package/src/index.ts +3 -0
- package/src/mapi.ts +1016 -0
- package/src/monetize.ts +67 -0
- package/test/connection.ts +43 -0
- package/test/exec-queries.ts +100 -0
- package/test/filetransfer.ts +94 -0
- package/test/prepare-statement.ts +27 -0
- package/test/query-stream.ts +41 -0
- package/test/tmp/.gitignore +4 -0
- package/tsconfig.json +24 -0
- package/.travis.yml +0 -11
- package/dist/mapi.d.ts +0 -58
- package/dist/mapi.js +0 -250
- package/dist/mapi.js.map +0 -1
- package/dist/tsconfig.tsbuildinfo +0 -1
- package/foo.js +0 -16
- package/index.js +0 -5
- package/src/mapi-connection.js +0 -784
- package/src/monetdb-connection.js +0 -385
- package/src/utils.js +0 -27
- package/test/common.js +0 -45
- package/test/install-monetdb.sh +0 -11
- package/test/monetdb_stream.js +0 -106
- package/test/start-monetdb.sh +0 -38
- package/test/test.js +0 -908
- package/test/test_connection.js +0 -290
- /package/docs/{README.v0.md → v0/README.v0.md} +0 -0
- /package/docs/{MapiConnection.md → v1/MapiConnection.md} +0 -0
- /package/docs/{v1-notes.md → v1/v1-notes.md} +0 -0
package/src/mapi.ts
ADDED
@@ -0,0 +1,1016 @@
|
|
1
|
+
import { Socket, SocketConnectOpts } from "node:net";
|
2
|
+
import { once, EventEmitter, Abortable } from "events";
|
3
|
+
import { Buffer, constants } from "buffer";
|
4
|
+
import { createHash } from "node:crypto";
|
5
|
+
import defaults from "./defaults";
|
6
|
+
import { URL } from "node:url";
|
7
|
+
import { FileUploader, FileDownloader } from "./file-transfer";
|
8
|
+
|
9
|
+
const MAPI_BLOCK_SIZE = 1024 * 8 - 2;
|
10
|
+
const MAPI_HEADER_SIZE = 2;
|
11
|
+
|
12
|
+
const MSG_PROMPT = "";
|
13
|
+
const MSG_MORE = "\x01\x02\n";
|
14
|
+
const MSG_FILETRANS = "\x01\x03\n";
|
15
|
+
const MSG_INFO = "#";
|
16
|
+
const MSG_ERROR = "!";
|
17
|
+
const MSG_Q = "&";
|
18
|
+
const MSG_QTABLE = "&1";
|
19
|
+
const MSG_QUPDATE = "&2";
|
20
|
+
const MSG_QSCHEMA = "&3";
|
21
|
+
const MSG_QTRANS = "&4";
|
22
|
+
const MSG_QPREPARE = "&5";
|
23
|
+
const MSG_QBLOCK = "&6";
|
24
|
+
const MSG_HEADER = "%";
|
25
|
+
const MSG_TUPLE = "[";
|
26
|
+
const MSG_TUPLE_NOSLICE = "=";
|
27
|
+
const MSG_REDIRECT = "^";
|
28
|
+
const MSG_OK = "=OK";
|
29
|
+
|
30
|
+
const MAX_REDIRECTS = 10;
|
31
|
+
const MAX_BUFF_SIZE = constants.MAX_LENGTH;
|
32
|
+
|
33
|
+
enum MAPI_STATE {
|
34
|
+
INIT = 1,
|
35
|
+
CONNECTED,
|
36
|
+
READY,
|
37
|
+
}
|
38
|
+
|
39
|
+
enum MAPI_LANGUAGE {
|
40
|
+
SQL = "sql",
|
41
|
+
MAPI = "mapi",
|
42
|
+
CONTROL = "control", // ? Not implemented
|
43
|
+
}
|
44
|
+
|
45
|
+
interface MapiConfig {
|
46
|
+
database: string;
|
47
|
+
username?: string;
|
48
|
+
password?: string;
|
49
|
+
language?: MAPI_LANGUAGE;
|
50
|
+
host?: string;
|
51
|
+
port?: number;
|
52
|
+
unixSocket?: string;
|
53
|
+
timeout?: number;
|
54
|
+
autoCommit?: boolean;
|
55
|
+
replySize?: number;
|
56
|
+
}
|
57
|
+
|
58
|
+
class HandShakeOption {
|
59
|
+
level: number;
|
60
|
+
name: string;
|
61
|
+
value: any;
|
62
|
+
fallback?: (v: any) => void;
|
63
|
+
sent: boolean;
|
64
|
+
constructor(
|
65
|
+
level: number,
|
66
|
+
name: string,
|
67
|
+
value: any,
|
68
|
+
fallback: (v: any) => void,
|
69
|
+
sent = false
|
70
|
+
) {
|
71
|
+
this.level = level;
|
72
|
+
this.name = name;
|
73
|
+
this.value = value;
|
74
|
+
this.fallback = fallback;
|
75
|
+
this.sent = sent;
|
76
|
+
}
|
77
|
+
}
|
78
|
+
|
79
|
+
function isMapiUri(uri: string): boolean {
|
80
|
+
const regx = new RegExp("^mapi:monetdb://*", "i");
|
81
|
+
return regx.test(uri);
|
82
|
+
}
|
83
|
+
|
84
|
+
function parseMapiUri(uri: string): MapiConfig {
|
85
|
+
if (isMapiUri(uri)) {
|
86
|
+
const url = new URL(uri.substring(5));
|
87
|
+
if (url.hostname) {
|
88
|
+
const host = url.hostname;
|
89
|
+
const port = parseInt(url.port);
|
90
|
+
const username = url.username;
|
91
|
+
const password = url.password;
|
92
|
+
const database = url.pathname.split("/")[1];
|
93
|
+
return {
|
94
|
+
host,
|
95
|
+
port,
|
96
|
+
username,
|
97
|
+
password,
|
98
|
+
database,
|
99
|
+
};
|
100
|
+
}
|
101
|
+
}
|
102
|
+
throw new Error(`Invalid MAPI URI ${uri}!`);
|
103
|
+
}
|
104
|
+
|
105
|
+
// validates and sets defaults on missing properties
|
106
|
+
function createMapiConfig(params: MapiConfig): MapiConfig {
|
107
|
+
const database =
|
108
|
+
params && params.database ? params.database : defaults.database;
|
109
|
+
if (typeof database != "string") {
|
110
|
+
throw new Error("database name must be string");
|
111
|
+
}
|
112
|
+
|
113
|
+
const username =
|
114
|
+
params && params.username ? params.username : defaults.username;
|
115
|
+
const password =
|
116
|
+
params && params.password ? params.password : defaults.password;
|
117
|
+
|
118
|
+
let host = params && params.host;
|
119
|
+
const unixSocket = params && params.unixSocket;
|
120
|
+
if (!unixSocket && !host) host = defaults.host;
|
121
|
+
if (typeof host != "string") {
|
122
|
+
throw new TypeError(`${host} is not valid hostname`);
|
123
|
+
}
|
124
|
+
const port =
|
125
|
+
params && params.port ? Number(params.port) : Number(defaults.port);
|
126
|
+
if (isNaN(port)) {
|
127
|
+
throw new TypeError(`${port} is not valid port`);
|
128
|
+
}
|
129
|
+
|
130
|
+
const timeout = params && params.timeout ? Number(params.timeout) : undefined;
|
131
|
+
if (timeout && isNaN(timeout)) {
|
132
|
+
throw new TypeError("timeout must be number");
|
133
|
+
}
|
134
|
+
const language =
|
135
|
+
params && params.language ? params.language : MAPI_LANGUAGE.SQL;
|
136
|
+
const autoCommit = params.autoCommit || defaults.autoCommit;
|
137
|
+
const replySize = params.replySize || defaults.replySize;
|
138
|
+
|
139
|
+
return {
|
140
|
+
database,
|
141
|
+
username,
|
142
|
+
password,
|
143
|
+
language,
|
144
|
+
host,
|
145
|
+
port,
|
146
|
+
timeout,
|
147
|
+
unixSocket,
|
148
|
+
autoCommit,
|
149
|
+
replySize,
|
150
|
+
};
|
151
|
+
}
|
152
|
+
|
153
|
+
class Column {
|
154
|
+
table: string;
|
155
|
+
name: string;
|
156
|
+
type: string;
|
157
|
+
length?: number;
|
158
|
+
index?: number;
|
159
|
+
constructor(
|
160
|
+
table: string,
|
161
|
+
name: string,
|
162
|
+
type: string,
|
163
|
+
index?: number,
|
164
|
+
length?: number
|
165
|
+
) {
|
166
|
+
this.table = table;
|
167
|
+
this.name = name;
|
168
|
+
this.type = type;
|
169
|
+
this.index = index;
|
170
|
+
this.length = length;
|
171
|
+
}
|
172
|
+
}
|
173
|
+
|
174
|
+
type QueryResult = {
|
175
|
+
id?: number;
|
176
|
+
type?: string;
|
177
|
+
queryId?: number;
|
178
|
+
rowCnt?: number;
|
179
|
+
affectedRows?: number;
|
180
|
+
columnCnt?: number;
|
181
|
+
queryTime?: number; // microseconds
|
182
|
+
sqlOptimizerTime?: number; // microseconds
|
183
|
+
malOptimizerTime?: number; // microseconds
|
184
|
+
columns?: Column[];
|
185
|
+
headers?: ResponseHeaders;
|
186
|
+
data?: any[];
|
187
|
+
};
|
188
|
+
|
189
|
+
class QueryStream extends EventEmitter {
|
190
|
+
constructor() {
|
191
|
+
super();
|
192
|
+
}
|
193
|
+
|
194
|
+
end(res?: QueryResult) {
|
195
|
+
this.emit("end", res);
|
196
|
+
}
|
197
|
+
}
|
198
|
+
|
199
|
+
function parseHeaderLine(hdrLine: string): Object {
|
200
|
+
if (hdrLine.startsWith(MSG_HEADER)) {
|
201
|
+
const [head, tail] = hdrLine.substring(1).trim().split("#");
|
202
|
+
let res = {};
|
203
|
+
const vals = head.trim().split(",\t");
|
204
|
+
switch (tail.trim()) {
|
205
|
+
case "table_name":
|
206
|
+
res = { tableNames: vals };
|
207
|
+
break;
|
208
|
+
case "name":
|
209
|
+
res = { columnNames: vals };
|
210
|
+
break;
|
211
|
+
case "type":
|
212
|
+
res = { columnTypes: vals };
|
213
|
+
break;
|
214
|
+
default:
|
215
|
+
res = {};
|
216
|
+
}
|
217
|
+
return res;
|
218
|
+
}
|
219
|
+
throw TypeError("Invalid header format!");
|
220
|
+
}
|
221
|
+
|
222
|
+
function parseTupleLine(line: string, types: string[]): any[] {
|
223
|
+
if (line.startsWith(MSG_TUPLE) && line.endsWith("]")) {
|
224
|
+
var resultline = [];
|
225
|
+
var cCol = 0;
|
226
|
+
var curtok = "";
|
227
|
+
var state = "INCRAP";
|
228
|
+
let endQuotes = 0;
|
229
|
+
/* mostly adapted from clients/R/MonetDB.R/src/mapisplit.c */
|
230
|
+
for (var curPos = 2; curPos < line.length - 1; curPos++) {
|
231
|
+
var chr = line.charAt(curPos);
|
232
|
+
switch (state) {
|
233
|
+
case "INCRAP":
|
234
|
+
if (chr != "\t" && chr != "," && chr != " ") {
|
235
|
+
if (chr == '"') {
|
236
|
+
state = "INQUOTES";
|
237
|
+
} else {
|
238
|
+
state = "INTOKEN";
|
239
|
+
curtok += chr;
|
240
|
+
}
|
241
|
+
}
|
242
|
+
break;
|
243
|
+
case "INTOKEN":
|
244
|
+
if (chr == "," || curPos == line.length - 2) {
|
245
|
+
if (curtok == "NULL" && endQuotes === 0) {
|
246
|
+
resultline.push(null);
|
247
|
+
} else {
|
248
|
+
switch (types[cCol]) {
|
249
|
+
case "boolean":
|
250
|
+
resultline.push(curtok == "true");
|
251
|
+
break;
|
252
|
+
case "tinyint":
|
253
|
+
case "smallint":
|
254
|
+
case "int":
|
255
|
+
case "wrd":
|
256
|
+
case "bigint":
|
257
|
+
resultline.push(parseInt(curtok));
|
258
|
+
break;
|
259
|
+
case "real":
|
260
|
+
case "double":
|
261
|
+
case "decimal":
|
262
|
+
resultline.push(parseFloat(curtok));
|
263
|
+
break;
|
264
|
+
case "json":
|
265
|
+
try {
|
266
|
+
resultline.push(JSON.parse(curtok));
|
267
|
+
} catch (e) {
|
268
|
+
resultline.push(curtok);
|
269
|
+
}
|
270
|
+
break;
|
271
|
+
default:
|
272
|
+
// we need to unescape double quotes
|
273
|
+
//valPtr = valPtr.replace(/[^\\]\\"/g, '"');
|
274
|
+
resultline.push(curtok);
|
275
|
+
break;
|
276
|
+
}
|
277
|
+
}
|
278
|
+
cCol++;
|
279
|
+
state = "INCRAP";
|
280
|
+
curtok = "";
|
281
|
+
endQuotes = 0;
|
282
|
+
} else {
|
283
|
+
curtok += chr;
|
284
|
+
}
|
285
|
+
break;
|
286
|
+
case "ESCAPED":
|
287
|
+
state = "INQUOTES";
|
288
|
+
switch (chr) {
|
289
|
+
case "t":
|
290
|
+
curtok += "\t";
|
291
|
+
break;
|
292
|
+
case "n":
|
293
|
+
curtok += "\n";
|
294
|
+
break;
|
295
|
+
case "r":
|
296
|
+
curtok += "\r";
|
297
|
+
break;
|
298
|
+
default:
|
299
|
+
curtok += chr;
|
300
|
+
}
|
301
|
+
break;
|
302
|
+
case "INQUOTES":
|
303
|
+
if (chr == '"') {
|
304
|
+
state = "INTOKEN";
|
305
|
+
endQuotes++;
|
306
|
+
break;
|
307
|
+
}
|
308
|
+
if (chr == "\\") {
|
309
|
+
state = "ESCAPED";
|
310
|
+
break;
|
311
|
+
}
|
312
|
+
curtok += chr;
|
313
|
+
break;
|
314
|
+
}
|
315
|
+
}
|
316
|
+
return resultline;
|
317
|
+
}
|
318
|
+
throw TypeError("Invalid tuple format!");
|
319
|
+
}
|
320
|
+
|
321
|
+
interface ResponseCallbacks {
|
322
|
+
resolve: (v: QueryResult | QueryStream | Promise<any>) => void;
|
323
|
+
reject: (err: Error) => void;
|
324
|
+
}
|
325
|
+
|
326
|
+
interface ResponseHeaders {
|
327
|
+
tableNames?: string[];
|
328
|
+
columnNames?: string[];
|
329
|
+
columnTypes?: string[];
|
330
|
+
}
|
331
|
+
|
332
|
+
interface ResponseOpt {
|
333
|
+
stream?: boolean;
|
334
|
+
callbacks?: ResponseCallbacks;
|
335
|
+
fileHandler?: any;
|
336
|
+
}
|
337
|
+
|
338
|
+
class Response {
|
339
|
+
buff: Buffer;
|
340
|
+
offset: number;
|
341
|
+
parseOffset: number;
|
342
|
+
stream: boolean;
|
343
|
+
settled: boolean;
|
344
|
+
segments: Segment[];
|
345
|
+
result?: QueryResult;
|
346
|
+
callbacks: ResponseCallbacks;
|
347
|
+
queryStream?: QueryStream;
|
348
|
+
headers?: ResponseHeaders;
|
349
|
+
fileHandler: any;
|
350
|
+
|
351
|
+
constructor(opt: ResponseOpt = {}) {
|
352
|
+
this.buff = Buffer.allocUnsafe(MAPI_BLOCK_SIZE).fill(0);
|
353
|
+
this.offset = 0;
|
354
|
+
this.parseOffset = 0;
|
355
|
+
this.segments = [];
|
356
|
+
this.settled = false;
|
357
|
+
this.stream = opt.stream;
|
358
|
+
this.callbacks = opt.callbacks;
|
359
|
+
this.fileHandler = opt.fileHandler;
|
360
|
+
if (opt.stream) {
|
361
|
+
this.queryStream = new QueryStream();
|
362
|
+
if (opt.callbacks && opt.callbacks.resolve)
|
363
|
+
opt.callbacks.resolve(this.queryStream);
|
364
|
+
}
|
365
|
+
}
|
366
|
+
|
367
|
+
append(data: Buffer): number {
|
368
|
+
let srcStartIndx = 0;
|
369
|
+
let srcEndIndx = srcStartIndx + data.length;
|
370
|
+
const l = this.segments.length;
|
371
|
+
let segment = (l > 0 && this.segments[l - 1]) || undefined;
|
372
|
+
let bytesCopied = 0;
|
373
|
+
let bytesProcessed = 0;
|
374
|
+
if (!this.complete()) {
|
375
|
+
// check if out of space
|
376
|
+
if (this.buff.length - this.offset < data.length) {
|
377
|
+
const bytes = this.expand(MAPI_BLOCK_SIZE);
|
378
|
+
console.log(`expanding by ${bytes} bytes!`);
|
379
|
+
}
|
380
|
+
|
381
|
+
if (segment === undefined || (segment && segment.isFull())) {
|
382
|
+
const hdr = data.readUInt16LE(0);
|
383
|
+
const last = (hdr & 1) === 1;
|
384
|
+
const bytes = hdr >> 1;
|
385
|
+
srcStartIndx = MAPI_HEADER_SIZE;
|
386
|
+
srcEndIndx = srcStartIndx + Math.min(bytes, data.length);
|
387
|
+
bytesCopied = data.copy(
|
388
|
+
this.buff,
|
389
|
+
this.offset,
|
390
|
+
srcStartIndx,
|
391
|
+
srcEndIndx
|
392
|
+
);
|
393
|
+
segment = new Segment(bytes, last, this.offset, bytesCopied);
|
394
|
+
this.segments.push(segment);
|
395
|
+
this.offset += bytesCopied;
|
396
|
+
bytesProcessed = MAPI_HEADER_SIZE + bytesCopied;
|
397
|
+
} else {
|
398
|
+
const byteCntToRead = segment.bytes - segment.bytesOffset;
|
399
|
+
srcEndIndx = srcStartIndx + byteCntToRead;
|
400
|
+
bytesCopied = data.copy(
|
401
|
+
this.buff,
|
402
|
+
this.offset,
|
403
|
+
srcStartIndx,
|
404
|
+
srcEndIndx
|
405
|
+
);
|
406
|
+
this.offset += bytesCopied;
|
407
|
+
segment.bytesOffset += bytesCopied;
|
408
|
+
// console.log(`segment is full ${segment.bytesOffset === segment.bytes}`);
|
409
|
+
bytesProcessed = bytesCopied;
|
410
|
+
}
|
411
|
+
if (this.isQueryResponse()) {
|
412
|
+
const tuples = [];
|
413
|
+
const firstPackage = this.parseOffset === 0;
|
414
|
+
this.parseOffset += this.parse(this.toString(this.parseOffset), tuples);
|
415
|
+
if (tuples.length > 0) {
|
416
|
+
if (this.queryStream) {
|
417
|
+
// emit header once
|
418
|
+
if (firstPackage && this.result && this.result.columns) {
|
419
|
+
this.queryStream.emit("header", this.result.columns);
|
420
|
+
}
|
421
|
+
// emit tuples
|
422
|
+
this.queryStream.emit("data", tuples);
|
423
|
+
} else {
|
424
|
+
this.result.data = this.result.data || [];
|
425
|
+
for (let t of tuples) {
|
426
|
+
this.result.data.push(t);
|
427
|
+
}
|
428
|
+
}
|
429
|
+
}
|
430
|
+
}
|
431
|
+
}
|
432
|
+
return bytesProcessed;
|
433
|
+
}
|
434
|
+
|
435
|
+
complete(): boolean {
|
436
|
+
const l = this.segments.length;
|
437
|
+
if (l > 0) {
|
438
|
+
const segment = this.segments[l - 1];
|
439
|
+
return segment.last && segment.isFull();
|
440
|
+
}
|
441
|
+
return false;
|
442
|
+
}
|
443
|
+
|
444
|
+
private seekOffset(): number {
|
445
|
+
const len = this.segments.length;
|
446
|
+
if (len) {
|
447
|
+
const last = this.segments[len - 1];
|
448
|
+
if (last.isFull()) return last.offset + last.bytes;
|
449
|
+
return last.offset;
|
450
|
+
}
|
451
|
+
return 0;
|
452
|
+
}
|
453
|
+
|
454
|
+
private expand(byteCount: number): number {
|
455
|
+
if (
|
456
|
+
this.buff.length + byteCount > MAX_BUFF_SIZE &&
|
457
|
+
this.fileHandler instanceof FileDownloader
|
458
|
+
) {
|
459
|
+
const offset = this.seekOffset();
|
460
|
+
if (offset) {
|
461
|
+
this.fileHandler.writeChunk(this.buff.subarray(0, offset));
|
462
|
+
this.buff = this.buff.subarray(offset);
|
463
|
+
this.offset -= offset;
|
464
|
+
}
|
465
|
+
}
|
466
|
+
const buff = Buffer.allocUnsafe(this.buff.length + byteCount).fill(0);
|
467
|
+
const bytesCopied = this.buff.copy(buff);
|
468
|
+
this.buff = buff;
|
469
|
+
// should be byteCount
|
470
|
+
return this.buff.length - bytesCopied;
|
471
|
+
}
|
472
|
+
|
473
|
+
private firstCharacter(): string {
|
474
|
+
return this.buff.toString("utf8", 0, 1);
|
475
|
+
}
|
476
|
+
|
477
|
+
errorMessage(): string {
|
478
|
+
if (this.firstCharacter() === MSG_ERROR) {
|
479
|
+
return this.buff.toString("utf8", 1);
|
480
|
+
}
|
481
|
+
return "";
|
482
|
+
}
|
483
|
+
|
484
|
+
isFileTransfer(): boolean {
|
485
|
+
return this.toString().startsWith(MSG_FILETRANS);
|
486
|
+
}
|
487
|
+
|
488
|
+
isPrompt(): boolean {
|
489
|
+
// perhaps use toString
|
490
|
+
return this.complete() && this.firstCharacter() === "\x00";
|
491
|
+
}
|
492
|
+
|
493
|
+
isRedirect(): boolean {
|
494
|
+
return this.firstCharacter() === MSG_REDIRECT;
|
495
|
+
}
|
496
|
+
|
497
|
+
isQueryResponse(): boolean {
|
498
|
+
if (this.result && this.result.type) {
|
499
|
+
return this.result.type.startsWith(MSG_Q);
|
500
|
+
}
|
501
|
+
return this.firstCharacter() === MSG_Q;
|
502
|
+
}
|
503
|
+
|
504
|
+
isMsgMore(): boolean {
|
505
|
+
// server wants more ?
|
506
|
+
return this.toString().startsWith(MSG_MORE);
|
507
|
+
}
|
508
|
+
|
509
|
+
toString(start?: number) {
|
510
|
+
const res = this.buff.toString("utf8", 0, this.offset);
|
511
|
+
if (start) return res.substring(start);
|
512
|
+
return res;
|
513
|
+
}
|
514
|
+
|
515
|
+
settle(res?: Promise<any>): void {
|
516
|
+
if (this.settled === false && this.complete()) {
|
517
|
+
const errMsg = this.errorMessage();
|
518
|
+
const err = errMsg ? new Error(errMsg) : null;
|
519
|
+
if (this.queryStream) {
|
520
|
+
if (err) this.queryStream.emit("error", err);
|
521
|
+
this.queryStream.end();
|
522
|
+
} else {
|
523
|
+
if (this.callbacks) {
|
524
|
+
if (err) {
|
525
|
+
this.callbacks.reject(err);
|
526
|
+
} else {
|
527
|
+
this.callbacks.resolve(res || this.result);
|
528
|
+
}
|
529
|
+
} else if (this.fileHandler && this.isQueryResponse()) {
|
530
|
+
this.fileHandler.resolve(this.result);
|
531
|
+
} else if (this.fileHandler && (err || this.fileHandler.err)) {
|
532
|
+
this.fileHandler.reject(err || this.fileHandler.err);
|
533
|
+
}
|
534
|
+
}
|
535
|
+
this.settled = true;
|
536
|
+
}
|
537
|
+
}
|
538
|
+
|
539
|
+
parse(data: string, res: any[]): number {
|
540
|
+
let offset = 0;
|
541
|
+
const lines = data.split("\n").length;
|
542
|
+
if (this.isQueryResponse()) {
|
543
|
+
let eol = data.indexOf("\n");
|
544
|
+
this.result = this.result || {};
|
545
|
+
if (
|
546
|
+
this.result.type === undefined &&
|
547
|
+
data.startsWith(MSG_Q) &&
|
548
|
+
lines > 0
|
549
|
+
) {
|
550
|
+
// process 1st line
|
551
|
+
const line = data.substring(0, eol);
|
552
|
+
this.result.type = line.substring(0, 2);
|
553
|
+
const rest = line.substring(3).trim().split(" ");
|
554
|
+
if (this.result.type === MSG_QTABLE) {
|
555
|
+
const [
|
556
|
+
id,
|
557
|
+
rowCnt,
|
558
|
+
columnCnt,
|
559
|
+
rows,
|
560
|
+
queryId,
|
561
|
+
queryTime,
|
562
|
+
malOptimizerTime,
|
563
|
+
sqlOptimizerTime,
|
564
|
+
] = rest;
|
565
|
+
this.result.id = parseInt(id);
|
566
|
+
this.result.rowCnt = parseInt(rowCnt);
|
567
|
+
this.result.columnCnt = parseInt(columnCnt);
|
568
|
+
this.result.queryId = parseInt(queryId);
|
569
|
+
this.result.queryTime = parseInt(queryTime);
|
570
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
571
|
+
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
572
|
+
} else if (this.result.type === MSG_QUPDATE) {
|
573
|
+
const [
|
574
|
+
affectedRowCnt,
|
575
|
+
autoIncrementId,
|
576
|
+
queryId,
|
577
|
+
queryTime,
|
578
|
+
malOptimizerTime,
|
579
|
+
sqlOptimizerTime,
|
580
|
+
] = rest;
|
581
|
+
this.result.affectedRows = parseInt(affectedRowCnt);
|
582
|
+
this.result.queryId = parseInt(queryId);
|
583
|
+
this.result.queryTime = parseInt(queryTime);
|
584
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
585
|
+
this.result.sqlOptimizerTime = parseInt(sqlOptimizerTime);
|
586
|
+
} else if (this.result.type === MSG_QSCHEMA) {
|
587
|
+
const [queryTime, malOptimizerTime] = rest;
|
588
|
+
this.result.queryTime = parseInt(queryTime);
|
589
|
+
this.result.malOptimizerTime = parseInt(malOptimizerTime);
|
590
|
+
} else if (this.result.type === MSG_QTRANS) {
|
591
|
+
// skip
|
592
|
+
} else if (this.result.type === MSG_QPREPARE) {
|
593
|
+
const [id, rowCnt, columnCnt, rows] = rest;
|
594
|
+
this.result.id = parseInt(id);
|
595
|
+
this.result.rowCnt = parseInt(rowCnt);
|
596
|
+
this.result.columnCnt = parseInt(columnCnt);
|
597
|
+
}
|
598
|
+
// end 1st line
|
599
|
+
|
600
|
+
if (
|
601
|
+
this.headers === undefined &&
|
602
|
+
data.charAt(eol + 1) === MSG_HEADER &&
|
603
|
+
lines > 5
|
604
|
+
) {
|
605
|
+
let headers: ResponseHeaders = {};
|
606
|
+
while (data.charAt(eol + 1) === MSG_HEADER) {
|
607
|
+
const hs = eol + 1;
|
608
|
+
eol = data.indexOf("\n", hs);
|
609
|
+
headers = {
|
610
|
+
...headers,
|
611
|
+
...parseHeaderLine(data.substring(hs, eol)),
|
612
|
+
};
|
613
|
+
}
|
614
|
+
this.headers = headers;
|
615
|
+
const colums: Column[] = [];
|
616
|
+
for (let i = 0; i < this.result.columnCnt; i++) {
|
617
|
+
const table = headers.tableNames && headers.tableNames[i];
|
618
|
+
const name = headers.columnNames && headers.columnNames[i];
|
619
|
+
const type = headers.columnTypes && headers.columnTypes[i];
|
620
|
+
colums.push({
|
621
|
+
table,
|
622
|
+
name,
|
623
|
+
type,
|
624
|
+
index: i,
|
625
|
+
});
|
626
|
+
}
|
627
|
+
this.result.columns = colums;
|
628
|
+
}
|
629
|
+
}
|
630
|
+
offset = eol + 1;
|
631
|
+
let ts: number = undefined; // tuple index
|
632
|
+
if (data.startsWith(MSG_TUPLE)) {
|
633
|
+
ts = 0;
|
634
|
+
} else if (data.charAt(eol + 1) === MSG_TUPLE) {
|
635
|
+
ts = eol + 1;
|
636
|
+
eol = data.indexOf("\n", ts);
|
637
|
+
}
|
638
|
+
if (ts !== undefined && eol > 0) {
|
639
|
+
// we have a data row
|
640
|
+
do {
|
641
|
+
offset = eol + 1;
|
642
|
+
const tuple = parseTupleLine(
|
643
|
+
data.substring(ts, eol),
|
644
|
+
this.headers.columnTypes
|
645
|
+
);
|
646
|
+
res.push(tuple);
|
647
|
+
if (data.charAt(eol + 1) === MSG_TUPLE) {
|
648
|
+
ts = eol + 1;
|
649
|
+
eol = data.indexOf("\n", ts);
|
650
|
+
} else {
|
651
|
+
ts = undefined;
|
652
|
+
}
|
653
|
+
} while (ts && eol > -1);
|
654
|
+
}
|
655
|
+
}
|
656
|
+
return offset;
|
657
|
+
}
|
658
|
+
}
|
659
|
+
|
660
|
+
class Segment {
|
661
|
+
offset: number; // where segment starts
|
662
|
+
bytes: number;
|
663
|
+
bytesOffset: number; // meaningful bytes e.g. if offset + bytesOffset == bytes, then segment full
|
664
|
+
last: boolean;
|
665
|
+
constructor(
|
666
|
+
bytes: number,
|
667
|
+
last: boolean,
|
668
|
+
offset: number,
|
669
|
+
bytesOffset: number
|
670
|
+
) {
|
671
|
+
this.bytes = bytes;
|
672
|
+
this.last = last;
|
673
|
+
this.offset = offset;
|
674
|
+
this.bytesOffset = bytesOffset;
|
675
|
+
}
|
676
|
+
|
677
|
+
isFull(): boolean {
|
678
|
+
return this.bytes === this.bytesOffset;
|
679
|
+
}
|
680
|
+
}
|
681
|
+
|
682
|
+
class MapiConnection extends EventEmitter {
|
683
|
+
state: MAPI_STATE;
|
684
|
+
socket: Socket;
|
685
|
+
database: string;
|
686
|
+
timeout: number;
|
687
|
+
username: string;
|
688
|
+
password: string;
|
689
|
+
host?: string;
|
690
|
+
unixSocket?: string;
|
691
|
+
port: number;
|
692
|
+
language: MAPI_LANGUAGE;
|
693
|
+
handShakeOptions?: HandShakeOption[];
|
694
|
+
redirects: number;
|
695
|
+
queue: Response[];
|
696
|
+
|
697
|
+
constructor(config: MapiConfig) {
|
698
|
+
super();
|
699
|
+
this.state = MAPI_STATE.INIT;
|
700
|
+
this.socket = this.createSocket(config.timeout);
|
701
|
+
// this.socket = new Socket();
|
702
|
+
// if (config.timeout) this.socket.setTimeout(config.timeout);
|
703
|
+
// this.socket.addListener("data", this.recv.bind(this));
|
704
|
+
// this.socket.addListener("error", this.handleSocketError.bind(this));
|
705
|
+
// this.socket.addListener("timeout", this.handleTimeout.bind(this));
|
706
|
+
// this.socket.addListener("close", () => {
|
707
|
+
// console.log("socket close event");
|
708
|
+
// this.emit("end");
|
709
|
+
// });
|
710
|
+
this.redirects = 0;
|
711
|
+
this.queue = [];
|
712
|
+
this.database = config.database;
|
713
|
+
this.language = config.language || MAPI_LANGUAGE.SQL;
|
714
|
+
this.unixSocket = config.unixSocket;
|
715
|
+
this.host = config.host;
|
716
|
+
this.port = config.port;
|
717
|
+
this.username = config.username;
|
718
|
+
this.password = config.password;
|
719
|
+
this.timeout = config.timeout;
|
720
|
+
}
|
721
|
+
|
722
|
+
private createSocket = (timeout?: number): Socket => {
|
723
|
+
const socket = new Socket();
|
724
|
+
if (timeout) socket.setTimeout(timeout);
|
725
|
+
socket.addListener("data", this.recv.bind(this));
|
726
|
+
socket.addListener("error", this.handleSocketError.bind(this));
|
727
|
+
socket.addListener("timeout", this.handleTimeout.bind(this));
|
728
|
+
socket.addListener("close", () => {
|
729
|
+
console.log("socket close event");
|
730
|
+
this.emit("end");
|
731
|
+
});
|
732
|
+
return socket;
|
733
|
+
};
|
734
|
+
|
735
|
+
connect(handShakeOptions: HandShakeOption[] = []): Promise<any[]> {
|
736
|
+
this.handShakeOptions = handShakeOptions;
|
737
|
+
// TODO unix socket
|
738
|
+
const opt: SocketConnectOpts = {
|
739
|
+
port: this.port,
|
740
|
+
host: this.host,
|
741
|
+
noDelay: true,
|
742
|
+
};
|
743
|
+
const socket =
|
744
|
+
this.socket && !this.socket.destroyed
|
745
|
+
? this.socket
|
746
|
+
: this.createSocket(this.timeout);
|
747
|
+
socket.connect(opt, () => {
|
748
|
+
this.state = MAPI_STATE.CONNECTED;
|
749
|
+
this.socket.setKeepAlive(true);
|
750
|
+
});
|
751
|
+
this.socket = socket;
|
752
|
+
|
753
|
+
return once(this, "ready");
|
754
|
+
}
|
755
|
+
|
756
|
+
ready(): boolean {
|
757
|
+
return this.state === MAPI_STATE.READY;
|
758
|
+
}
|
759
|
+
|
760
|
+
disconnect(): Promise<boolean> {
|
761
|
+
return new Promise((resolve, reject) => {
|
762
|
+
this.socket.end(() => {
|
763
|
+
this.redirects = 0;
|
764
|
+
this.state = MAPI_STATE.INIT;
|
765
|
+
this.socket.destroy();
|
766
|
+
resolve(this.state === MAPI_STATE.INIT);
|
767
|
+
});
|
768
|
+
});
|
769
|
+
}
|
770
|
+
|
771
|
+
private login(challenge: string): void {
|
772
|
+
const challengeParts = challenge.split(":");
|
773
|
+
const [salt, identity, protocol, hashes, endian, algo, opt_level] =
|
774
|
+
challengeParts;
|
775
|
+
let password: string;
|
776
|
+
try {
|
777
|
+
password = createHash(algo).update(this.password).digest("hex");
|
778
|
+
} catch (err) {
|
779
|
+
console.error(err);
|
780
|
+
this.emit("error", new TypeError(`Algorithm ${algo} not supported`));
|
781
|
+
return;
|
782
|
+
}
|
783
|
+
let pwhash = null;
|
784
|
+
// try hash algorithms in the order provided by the server
|
785
|
+
for (const algo of hashes.split(",")) {
|
786
|
+
try {
|
787
|
+
const hash = createHash(algo);
|
788
|
+
pwhash = `{${algo}}` + hash.update(password + salt).digest("hex");
|
789
|
+
break;
|
790
|
+
} catch {}
|
791
|
+
}
|
792
|
+
if (pwhash) {
|
793
|
+
let counterResponse = `LIT:${this.username}:${pwhash}:${this.language}:${this.database}:`;
|
794
|
+
if (opt_level && opt_level.startsWith("sql=")) {
|
795
|
+
let level = 0;
|
796
|
+
counterResponse += "FILETRANS:";
|
797
|
+
try {
|
798
|
+
level = Number(opt_level.substring(4));
|
799
|
+
} catch (err) {
|
800
|
+
this.emit(
|
801
|
+
"error",
|
802
|
+
new TypeError("Invalid handshake options level in server challenge")
|
803
|
+
);
|
804
|
+
return;
|
805
|
+
}
|
806
|
+
// process handshake options
|
807
|
+
const options = [];
|
808
|
+
for (const opt of this.handShakeOptions) {
|
809
|
+
if (opt.level < level) {
|
810
|
+
options.push(`${opt.name}=${Number(opt.value)}`);
|
811
|
+
opt.sent = true;
|
812
|
+
}
|
813
|
+
}
|
814
|
+
if (options) counterResponse += options.join(",") + ":";
|
815
|
+
}
|
816
|
+
this.send(Buffer.from(counterResponse))
|
817
|
+
.then(() => this.queue.push(new Response()))
|
818
|
+
.catch((err) => this.emit("error", err));
|
819
|
+
} else {
|
820
|
+
this.emit(
|
821
|
+
"error",
|
822
|
+
new TypeError(`None of the hashes ${hashes} are supported`)
|
823
|
+
);
|
824
|
+
}
|
825
|
+
}
|
826
|
+
|
827
|
+
/**
|
828
|
+
* Raise exception on server by sending bad packet
|
829
|
+
*/
|
830
|
+
requestAbort(): Promise<void> {
|
831
|
+
return new Promise((resolve, reject) => {
|
832
|
+
const header = Buffer.allocUnsafe(2).fill(0);
|
833
|
+
// larger than allowed and not final message
|
834
|
+
header.writeUint16LE(((2 * MAPI_BLOCK_SIZE) << 1) | 0, 0);
|
835
|
+
// invalid utf8 and too small
|
836
|
+
const badBody = Buffer.concat([
|
837
|
+
Buffer.from("ERROR"),
|
838
|
+
Buffer.from([0x80]),
|
839
|
+
]);
|
840
|
+
const outBuff = Buffer.concat([header, badBody]);
|
841
|
+
this.socket.write(outBuff, async (err?: Error) => {
|
842
|
+
if (err) reject(err);
|
843
|
+
resolve();
|
844
|
+
});
|
845
|
+
});
|
846
|
+
}
|
847
|
+
|
848
|
+
send(buff: Buffer): Promise<void> {
|
849
|
+
return new Promise((resolve, reject) => {
|
850
|
+
let last = 0;
|
851
|
+
let offset = 0;
|
852
|
+
while (last === 0) {
|
853
|
+
const seg = buff.subarray(offset, offset + MAPI_BLOCK_SIZE);
|
854
|
+
last = seg.length < MAPI_BLOCK_SIZE ? 1 : 0;
|
855
|
+
const header = Buffer.allocUnsafe(2).fill(0);
|
856
|
+
header.writeUint16LE((seg.length << 1) | last, 0);
|
857
|
+
const outBuff = Buffer.concat([header, seg]);
|
858
|
+
this.socket.write(outBuff, (err?: Error) => {
|
859
|
+
if (err) reject(err);
|
860
|
+
if (last) resolve();
|
861
|
+
});
|
862
|
+
offset += seg.length;
|
863
|
+
}
|
864
|
+
});
|
865
|
+
}
|
866
|
+
|
867
|
+
private handleTimeout() {
|
868
|
+
this.emit("error", new Error("Timeout"));
|
869
|
+
}
|
870
|
+
|
871
|
+
private handleSocketError(err: Error) {
|
872
|
+
console.error(err);
|
873
|
+
}
|
874
|
+
|
875
|
+
async request(
|
876
|
+
sql: string,
|
877
|
+
stream: boolean = false
|
878
|
+
): Promise<QueryResult | QueryStream> {
|
879
|
+
if (this.ready() === false) throw new Error("Not Connected");
|
880
|
+
await this.send(Buffer.from(sql));
|
881
|
+
return new Promise((resolve, reject) => {
|
882
|
+
const resp = new Response({
|
883
|
+
stream,
|
884
|
+
callbacks: { resolve, reject },
|
885
|
+
});
|
886
|
+
this.queue.push(resp);
|
887
|
+
});
|
888
|
+
}
|
889
|
+
|
890
|
+
async requestFileTransfer(buff: Buffer, fileHandler: any): Promise<void> {
|
891
|
+
await this.send(buff);
|
892
|
+
const resp = new Response({ fileHandler });
|
893
|
+
this.queue.push(resp);
|
894
|
+
}
|
895
|
+
|
896
|
+
async requestFileTransferError(err: string, fileHandler: any): Promise<void> {
|
897
|
+
await this.send(Buffer.from(err));
|
898
|
+
const resp = new Response({ fileHandler });
|
899
|
+
this.queue.push(resp);
|
900
|
+
}
|
901
|
+
|
902
|
+
private recv(data: Buffer): void {
|
903
|
+
let bytesLeftOver: number;
|
904
|
+
let resp: Response;
|
905
|
+
// process queue left to right, find 1st uncomplete response
|
906
|
+
// remove responses that are completed
|
907
|
+
while (this.queue.length) {
|
908
|
+
const next = this.queue[0];
|
909
|
+
if (next.complete() || next.settled) {
|
910
|
+
this.queue.shift();
|
911
|
+
} else {
|
912
|
+
resp = next;
|
913
|
+
break;
|
914
|
+
}
|
915
|
+
}
|
916
|
+
if (resp === undefined && this.queue.length === 0) {
|
917
|
+
// challenge message
|
918
|
+
// or direct call to send has being made
|
919
|
+
// e.g. request api appends Response to the queue
|
920
|
+
resp = new Response();
|
921
|
+
this.queue.push(resp);
|
922
|
+
}
|
923
|
+
|
924
|
+
const offset = resp.append(data);
|
925
|
+
|
926
|
+
if (resp.complete()) this.handleResponse(resp);
|
927
|
+
bytesLeftOver = data.length - offset;
|
928
|
+
if (bytesLeftOver) {
|
929
|
+
const msg = `some ${bytesLeftOver} bytes left over!`;
|
930
|
+
console.warn(msg);
|
931
|
+
this.recv(data.subarray(offset));
|
932
|
+
}
|
933
|
+
}
|
934
|
+
|
935
|
+
private handleResponse(resp: Response): void {
|
936
|
+
const err = resp.errorMessage();
|
937
|
+
if (this.state == MAPI_STATE.CONNECTED) {
|
938
|
+
if (err) {
|
939
|
+
this.emit("error", new Error(err));
|
940
|
+
return;
|
941
|
+
}
|
942
|
+
if (resp.isRedirect()) {
|
943
|
+
this.redirects += 1;
|
944
|
+
if (this.redirects > MAX_REDIRECTS)
|
945
|
+
this.emit(
|
946
|
+
"error",
|
947
|
+
new Error(`Exceeded max number of redirects ${MAX_REDIRECTS}`)
|
948
|
+
);
|
949
|
+
return;
|
950
|
+
}
|
951
|
+
if (resp.isPrompt()) {
|
952
|
+
console.log("login OK");
|
953
|
+
this.state = MAPI_STATE.READY;
|
954
|
+
this.emit("ready", this.state);
|
955
|
+
return;
|
956
|
+
}
|
957
|
+
return this.login(resp.toString());
|
958
|
+
}
|
959
|
+
|
960
|
+
if (resp.isFileTransfer()) {
|
961
|
+
console.log("file transfer");
|
962
|
+
let fhandler: any;
|
963
|
+
const msg = resp.toString(MSG_FILETRANS.length).trim();
|
964
|
+
let mode: string, offset: string, file: string;
|
965
|
+
if (msg.startsWith("r ")) {
|
966
|
+
[mode, offset, file] = msg.split(" ");
|
967
|
+
fhandler =
|
968
|
+
resp.fileHandler || new FileUploader(this, file, parseInt(offset));
|
969
|
+
return resp.settle(fhandler.upload());
|
970
|
+
} else if (msg.startsWith("rb")) {
|
971
|
+
[mode, file] = msg.split(" ");
|
972
|
+
fhandler = resp.fileHandler || new FileUploader(this, file, 0);
|
973
|
+
return resp.settle(fhandler.upload());
|
974
|
+
} else if (msg.startsWith("w")) {
|
975
|
+
[mode, file] = msg.split(" ");
|
976
|
+
fhandler = resp.fileHandler || new FileDownloader(this, file);
|
977
|
+
return resp.settle(fhandler.download());
|
978
|
+
} else {
|
979
|
+
// no msg end of transfer
|
980
|
+
const fileHandler = resp.fileHandler;
|
981
|
+
// we do expect a final response from server
|
982
|
+
this.queue.push(new Response({ fileHandler }));
|
983
|
+
return resp.settle(fileHandler.close());
|
984
|
+
}
|
985
|
+
}
|
986
|
+
|
987
|
+
if (resp.isMsgMore()) {
|
988
|
+
console.log("server wants more");
|
989
|
+
if (resp.fileHandler instanceof FileUploader)
|
990
|
+
return resp.settle(resp.fileHandler.upload());
|
991
|
+
}
|
992
|
+
|
993
|
+
if (
|
994
|
+
resp.fileHandler instanceof FileDownloader &&
|
995
|
+
resp.fileHandler.ready()
|
996
|
+
) {
|
997
|
+
// end of download
|
998
|
+
const fileHandler = resp.fileHandler;
|
999
|
+
fileHandler.writeChunk(resp.buff);
|
1000
|
+
// we do expect a final response from server
|
1001
|
+
this.queue.push(new Response({ fileHandler }));
|
1002
|
+
return resp.settle(fileHandler.close());
|
1003
|
+
}
|
1004
|
+
resp.settle();
|
1005
|
+
}
|
1006
|
+
}
|
1007
|
+
|
1008
|
+
export {
|
1009
|
+
MapiConfig,
|
1010
|
+
MapiConnection,
|
1011
|
+
parseMapiUri,
|
1012
|
+
createMapiConfig,
|
1013
|
+
HandShakeOption,
|
1014
|
+
QueryResult,
|
1015
|
+
QueryStream,
|
1016
|
+
};
|