node-firebird 2.12.0 → 2.13.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/README.md +96 -6
- package/lib/types.d.ts +38 -5
- package/lib/utils.d.ts +12 -0
- package/lib/utils.js +20 -2
- package/lib/wire/batch-stream.d.ts +26 -0
- package/lib/wire/batch-stream.js +109 -0
- package/lib/wire/codepages.d.ts +23 -0
- package/lib/wire/codepages.js +137 -0
- package/lib/wire/connection.d.ts +38 -4
- package/lib/wire/connection.js +294 -44
- package/lib/wire/database.d.ts +9 -0
- package/lib/wire/database.js +13 -6
- package/lib/wire/serialize.d.ts +2 -0
- package/lib/wire/serialize.js +10 -0
- package/lib/wire/socket.js +9 -3
- package/lib/wire/transaction.d.ts +6 -0
- package/lib/wire/transaction.js +27 -2
- package/lib/wire/xsqlvar.d.ts +56 -1
- package/lib/wire/xsqlvar.js +107 -29
- package/package.json +19 -1
- package/src/types.ts +40 -5
- package/src/utils.ts +19 -1
- package/src/wire/batch-stream.ts +121 -0
- package/src/wire/codepages.ts +147 -0
- package/src/wire/connection.ts +315 -49
- package/src/wire/database.ts +15 -7
- package/src/wire/serialize.ts +11 -0
- package/src/wire/socket.ts +9 -3
- package/src/wire/transaction.ts +28 -2
- package/src/wire/xsqlvar.ts +129 -30
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* batchStream — object-mode Writable over the Firebird 4 batch API
|
|
4
|
+
*
|
|
5
|
+
* The COPY FROM analogue: write parameter rows, they are flushed in
|
|
6
|
+
* chunks through statement.executeBatch (single prepared statement,
|
|
7
|
+
* protocol-level batching, BLOB values included). Backpressure is the
|
|
8
|
+
* Writable machinery itself: a write callback is held while a chunk
|
|
9
|
+
* is in flight.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
|
|
13
|
+
import { Writable } from 'stream';
|
|
14
|
+
import { fromCallback } from '../callback';
|
|
15
|
+
import { batchResultToError } from '../utils';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Build the Writable for Database.batchStream / Transaction.batchStream.
|
|
19
|
+
* With `ownsTransaction` (the Database form) the stream runs its own
|
|
20
|
+
* transaction: committed on finish, rolled back on error/destroy —
|
|
21
|
+
* all-or-nothing for the whole stream. The Transaction form leaves
|
|
22
|
+
* commit/rollback to the caller.
|
|
23
|
+
*
|
|
24
|
+
* Rows accumulate up to options.flushRows (default 1000) per
|
|
25
|
+
* executeBatch flush; the remaining executeBatch options (chunkSize,
|
|
26
|
+
* bufferSize, …) pass through. After 'finish', stream.recordCount and
|
|
27
|
+
* stream.affectedRows carry the totals.
|
|
28
|
+
*/
|
|
29
|
+
function makeBatchStream(target: any, query: string, options: any, ownsTransaction: boolean): Writable {
|
|
30
|
+
options = options || {};
|
|
31
|
+
const flushRows = options.flushRows > 0 ? Math.floor(options.flushRows) : 1000;
|
|
32
|
+
|
|
33
|
+
const batchOptions = { ...options };
|
|
34
|
+
delete batchOptions.flushRows;
|
|
35
|
+
delete batchOptions.highWaterMark;
|
|
36
|
+
|
|
37
|
+
let transaction: any = null;
|
|
38
|
+
let statement: any = null;
|
|
39
|
+
let buffered: any[][] = [];
|
|
40
|
+
|
|
41
|
+
const init = async () => {
|
|
42
|
+
if (statement) {
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
transaction = ownsTransaction ? await target.transactionAsync() : target;
|
|
46
|
+
statement = await fromCallback((cb) => transaction.newStatement(query, cb));
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
const flush = async () => {
|
|
50
|
+
if (!buffered.length) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await init();
|
|
54
|
+
const chunk = buffered;
|
|
55
|
+
buffered = [];
|
|
56
|
+
const result: any = await fromCallback((cb) =>
|
|
57
|
+
statement.executeBatch(transaction, chunk, cb, batchOptions));
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
// the same all-or-nothing error shape database.executeBatch uses
|
|
60
|
+
throw batchResultToError(result);
|
|
61
|
+
}
|
|
62
|
+
(stream as any).recordCount += result.recordCount;
|
|
63
|
+
for (const count of result.updateCounts) {
|
|
64
|
+
(stream as any).affectedRows += count;
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const cleanup = async (commit: boolean) => {
|
|
69
|
+
if (statement) {
|
|
70
|
+
const stmt = statement;
|
|
71
|
+
statement = null;
|
|
72
|
+
await new Promise<void>((resolve) => stmt.release(() => resolve()));
|
|
73
|
+
}
|
|
74
|
+
if (ownsTransaction && transaction) {
|
|
75
|
+
const tx = transaction;
|
|
76
|
+
transaction = null;
|
|
77
|
+
await (commit ? tx.commitAsync() : tx.rollbackAsync());
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const stream = new Writable({
|
|
82
|
+
objectMode: true,
|
|
83
|
+
highWaterMark: options.highWaterMark > 0 ? options.highWaterMark : flushRows,
|
|
84
|
+
|
|
85
|
+
write(row: any, _enc: any, cb: (err?: any) => void) {
|
|
86
|
+
if (!Array.isArray(row)) {
|
|
87
|
+
cb(new Error('batchStream expects parameter-array rows'));
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
buffered.push(row);
|
|
91
|
+
if (buffered.length >= flushRows) {
|
|
92
|
+
flush().then(() => cb(), cb);
|
|
93
|
+
} else {
|
|
94
|
+
cb();
|
|
95
|
+
}
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
final(cb: (err?: any) => void) {
|
|
99
|
+
// an empty stream finishes without touching the server at all
|
|
100
|
+
// (flush() early-returns and init never runs)
|
|
101
|
+
flush()
|
|
102
|
+
.then(() => cleanup(true))
|
|
103
|
+
.then(() => cb(), (err) => {
|
|
104
|
+
// the failed stream must not commit half a bulk load
|
|
105
|
+
cleanup(false).catch(() => { /* rollback best-effort */ });
|
|
106
|
+
cb(err);
|
|
107
|
+
});
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
destroy(err: any, cb: (err?: any) => void) {
|
|
111
|
+
cleanup(false)
|
|
112
|
+
.then(() => cb(err), () => cb(err));
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
(stream as any).recordCount = 0;
|
|
117
|
+
(stream as any).affectedRows = 0;
|
|
118
|
+
return stream;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export = makeBatchStream;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/***************************************
|
|
2
|
+
*
|
|
3
|
+
* Single-byte codepage codecs (WIN125x, ISO8859_x, KOI8, DOS866)
|
|
4
|
+
*
|
|
5
|
+
* Node's Buffer only decodes utf8/latin1/ascii natively. These
|
|
6
|
+
* codepages are decoded through the WHATWG TextDecoder (backed by
|
|
7
|
+
* ICU — present in every official Node build) and encoded through
|
|
8
|
+
* reverse tables built from the same decoder at first use, so the
|
|
9
|
+
* two directions can never disagree. Issues #319/#301/#422.
|
|
10
|
+
*
|
|
11
|
+
***************************************/
|
|
12
|
+
|
|
13
|
+
export interface TextCodec {
|
|
14
|
+
/** Firebird charset name (upper case). */
|
|
15
|
+
name: string;
|
|
16
|
+
decode(buffer: Buffer): string;
|
|
17
|
+
encode(value: string): Buffer;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Firebird charset name → WHATWG encoding label (single-byte only). */
|
|
21
|
+
const ICU_LABELS: Readonly<Record<string, string>> = Object.freeze({
|
|
22
|
+
WIN1250: 'windows-1250',
|
|
23
|
+
WIN1251: 'windows-1251',
|
|
24
|
+
WIN1253: 'windows-1253',
|
|
25
|
+
WIN1254: 'windows-1254',
|
|
26
|
+
WIN1255: 'windows-1255',
|
|
27
|
+
WIN1256: 'windows-1256',
|
|
28
|
+
WIN1257: 'windows-1257',
|
|
29
|
+
WIN1258: 'windows-1258',
|
|
30
|
+
ISO8859_2: 'iso-8859-2',
|
|
31
|
+
ISO8859_3: 'iso-8859-3',
|
|
32
|
+
ISO8859_4: 'iso-8859-4',
|
|
33
|
+
ISO8859_5: 'iso-8859-5',
|
|
34
|
+
ISO8859_6: 'iso-8859-6',
|
|
35
|
+
ISO8859_7: 'iso-8859-7',
|
|
36
|
+
ISO8859_8: 'iso-8859-8',
|
|
37
|
+
ISO8859_9: 'iso-8859-9',
|
|
38
|
+
ISO8859_13: 'iso-8859-13',
|
|
39
|
+
KOI8R: 'koi8-r',
|
|
40
|
+
KOI8U: 'koi8-u',
|
|
41
|
+
DOS866: 'ibm866',
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Bytes-per-character by Firebird charset id (RDB$CHARACTER_SETS —
|
|
46
|
+
* verified against a live server). Everything not listed (NONE, ASCII,
|
|
47
|
+
* ISO8859_x, WIN125x, DOS*, KOI8*, CYRL, TIS620, …) is single-byte.
|
|
48
|
+
*/
|
|
49
|
+
const CHARSET_WIDTH_BY_ID: Readonly<Record<number, number>> = Object.freeze({
|
|
50
|
+
3: 3, // UNICODE_FSS
|
|
51
|
+
4: 4, // UTF8
|
|
52
|
+
5: 2, // SJIS_0208
|
|
53
|
+
6: 2, // EUCJ_0208
|
|
54
|
+
44: 2, // KSC_5601
|
|
55
|
+
56: 2, // BIG_5
|
|
56
|
+
57: 2, // GB_2312
|
|
57
|
+
67: 2, // GBK
|
|
58
|
+
68: 2, // CP943C
|
|
59
|
+
69: 4, // GB18030
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export function charsetWidthById(id: number | undefined): number {
|
|
63
|
+
if (id === undefined) {
|
|
64
|
+
return 1;
|
|
65
|
+
}
|
|
66
|
+
return CHARSET_WIDTH_BY_ID[id] || 1;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cache = new Map<string, TextCodec | null>();
|
|
70
|
+
|
|
71
|
+
function buildCodec(name: string): TextCodec | null {
|
|
72
|
+
const label = ICU_LABELS[name];
|
|
73
|
+
if (!label) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
let decoder: TextDecoder;
|
|
77
|
+
try {
|
|
78
|
+
decoder = new TextDecoder(label);
|
|
79
|
+
} catch {
|
|
80
|
+
// Node built with small-icu: legacy encodings unavailable
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Build both directions from the decoder, one byte at a time — every
|
|
85
|
+
// byte of a single-byte codepage maps to exactly one BMP character
|
|
86
|
+
// (undefined bytes decode to U+FFFD, which is kept for decoding but
|
|
87
|
+
// never used for the reverse map).
|
|
88
|
+
const toCode = new Uint16Array(256);
|
|
89
|
+
const toByte = new Map<string, number>();
|
|
90
|
+
const one = Buffer.alloc(1);
|
|
91
|
+
for (let i = 0; i < 256; i++) {
|
|
92
|
+
one[0] = i;
|
|
93
|
+
const ch = decoder.decode(one);
|
|
94
|
+
toCode[i] = ch.charCodeAt(0);
|
|
95
|
+
if (ch !== '�' && !toByte.has(ch)) {
|
|
96
|
+
toByte.set(ch, i);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
name,
|
|
102
|
+
decode(buffer: Buffer): string {
|
|
103
|
+
// batch through fromCharCode instead of per-byte string concat —
|
|
104
|
+
// wide CHAR columns and text blobs decode in O(chunks) allocations
|
|
105
|
+
const codes = new Array<number>(buffer.length);
|
|
106
|
+
for (let i = 0; i < buffer.length; i++) {
|
|
107
|
+
codes[i] = toCode[buffer[i]];
|
|
108
|
+
}
|
|
109
|
+
const CHUNK = 4096;
|
|
110
|
+
if (codes.length <= CHUNK) {
|
|
111
|
+
return String.fromCharCode(...codes);
|
|
112
|
+
}
|
|
113
|
+
let out = '';
|
|
114
|
+
for (let i = 0; i < codes.length; i += CHUNK) {
|
|
115
|
+
out += String.fromCharCode(...codes.slice(i, i + CHUNK));
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
},
|
|
119
|
+
encode(value: string): Buffer {
|
|
120
|
+
const out = Buffer.alloc(value.length);
|
|
121
|
+
for (let i = 0; i < value.length; i++) {
|
|
122
|
+
const b = toByte.get(value[i]);
|
|
123
|
+
// unmappable characters become '?' — the convention every
|
|
124
|
+
// codepage transcoder (incl. iconv) uses by default
|
|
125
|
+
out[i] = b === undefined ? 0x3f : b;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Codec for a Firebird charset name, or null when the charset is unknown,
|
|
134
|
+
* natively handled by Buffer, or the ICU tables are unavailable. Cached.
|
|
135
|
+
*/
|
|
136
|
+
export function getCodec(charsetName: string | undefined): TextCodec | null {
|
|
137
|
+
if (!charsetName) {
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
140
|
+
const name = String(charsetName).toUpperCase();
|
|
141
|
+
let codec = cache.get(name);
|
|
142
|
+
if (codec === undefined) {
|
|
143
|
+
codec = buildCodec(name);
|
|
144
|
+
cache.set(name, codec);
|
|
145
|
+
}
|
|
146
|
+
return codec;
|
|
147
|
+
}
|