Skip to content

Commit 3110bbf

Browse files
authored
Add ConnectOption charSetForNONE (#165)
1 parent 63cf403 commit 3110bbf

File tree

10 files changed

+154
-9
lines changed

10 files changed

+154
-9
lines changed

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ node_modules/
22
.yarn/install-state.gz
33
dist/
44

5-
lerna-debug.log
6-
yarn-error.log
5+
*.log
76
tsconfig.tsbuildinfo
87
.env

packages/node-firebird-driver-native/src/lib/attachment.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ export class AttachmentImpl extends AbstractAttachment {
2727

2828
static async connect(client: ClientImpl, uri: string, options?: ConnectOptions): Promise<AttachmentImpl> {
2929
const attachment = new AttachmentImpl(client);
30+
attachment.charSetForNONE = options?.charSetForNONE ?? 'utf8';
3031

3132
return await client.statusAction(async (status) => {
3233
const dpb = createDpb(options);
@@ -41,6 +42,7 @@ export class AttachmentImpl extends AbstractAttachment {
4142
options?: CreateDatabaseOptions,
4243
): Promise<AttachmentImpl> {
4344
const attachment = new AttachmentImpl(client);
45+
attachment.charSetForNONE = options?.charSetForNONE ?? 'utf8';
4446

4547
return await client.statusAction(async (status) => {
4648
const dpb = createDpb(options);

packages/node-firebird-driver-native/src/lib/fb-util.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ export function createDescriptors(status: fb.Status, metadata?: fb.MessageMetada
7373
ret.push({
7474
type: metadata.getTypeSync(status, i),
7575
subType: metadata.getSubTypeSync(status, i),
76+
charSet: metadata.getCharSetSync(status, i),
7677
nullOffset: metadata.getNullOffsetSync(status, i),
7778
offset: metadata.getOffsetSync(status, i),
7879
length: metadata.getLengthSync(status, i),

packages/node-firebird-driver/package.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,13 @@
3131
"typings": "./dist/lib/index.d.ts",
3232
"dependencies": {
3333
"@types/node": "^22.13.10"
34+
},
35+
"peerDependencies": {
36+
"iconv-lite": "^0.7.2"
37+
},
38+
"peerDependenciesMeta": {
39+
"iconv-lite": {
40+
"optional": true
41+
}
3442
}
3543
}

packages/node-firebird-driver/src/lib/impl/attachment.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export abstract class AbstractAttachment implements Attachment {
2222
events = new Set<Events>();
2323
statements = new Set<AbstractStatement>();
2424
transactions = new Set<AbstractTransaction>();
25+
charSetForNONE = 'utf8';
2526

2627
/** Default transaction options. */
2728
defaultTransactionOptions: TransactionOptions;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
interface IconvLiteModule {
2+
encodingExists(encoding: string): boolean;
3+
decode(buffer: Buffer, encoding: string): string;
4+
encode(value: string, encoding: string): Buffer;
5+
}
6+
7+
let iconvLite: IconvLiteModule | undefined;
8+
let iconvLiteLoadAttempted = false;
9+
10+
function getIconvLite(encoding: string): IconvLiteModule {
11+
if (!iconvLiteLoadAttempted) {
12+
iconvLiteLoadAttempted = true;
13+
14+
try {
15+
// eslint-disable-next-line @typescript-eslint/no-require-imports
16+
iconvLite = require('iconv-lite') as IconvLiteModule;
17+
} catch {
18+
iconvLite = undefined;
19+
}
20+
}
21+
22+
if (!iconvLite) {
23+
throw new Error(
24+
`Encoding '${encoding}' in charSetForNONE requires optional dependency 'iconv-lite'. ` +
25+
`Install it with: yarn add iconv-lite`,
26+
);
27+
}
28+
29+
return iconvLite;
30+
}
31+
32+
export function decodeString(bytes: Buffer, encoding: string): string {
33+
if (Buffer.isEncoding(encoding as BufferEncoding)) {
34+
return bytes.toString(encoding as BufferEncoding);
35+
}
36+
37+
const iconvLiteModule = getIconvLite(encoding);
38+
39+
if (iconvLiteModule.encodingExists(encoding)) {
40+
return iconvLiteModule.decode(bytes, encoding);
41+
}
42+
43+
throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`);
44+
}
45+
46+
export function encodeString(value: string, encoding: string): Buffer {
47+
if (Buffer.isEncoding(encoding as BufferEncoding)) {
48+
return Buffer.from(value, encoding as BufferEncoding);
49+
}
50+
51+
const iconvLiteModule = getIconvLite(encoding);
52+
53+
if (iconvLiteModule.encodingExists(encoding)) {
54+
return iconvLiteModule.encode(value, encoding);
55+
}
56+
57+
throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`);
58+
}

packages/node-firebird-driver/src/lib/impl/fb-util.ts

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import * as os from 'os';
22
const littleEndian = os.endianness() === 'LE';
33

4-
import * as stringDecoder from 'string_decoder';
5-
64
import { AbstractAttachment } from './attachment';
5+
import { decodeString, encodeString } from './encoding';
76
import { decodeDate, decodeTime, encodeDate, encodeTime } from './date-time';
87
import { tzIdToString, tzStringToId } from './time-zones';
98
import { AbstractTransaction } from './transaction';
@@ -129,6 +128,7 @@ export namespace cancelType {
129128
}
130129

131130
export namespace charSets {
131+
export const none = 0;
132132
export const ascii = 2;
133133
}
134134

@@ -296,6 +296,7 @@ export function getPortableInteger(buffer: Uint8Array, length: number) {
296296
export interface Descriptor {
297297
type: number;
298298
subType: number;
299+
charSet: number;
299300
length: number;
300301
scale: number;
301302
offset: number;
@@ -326,11 +327,11 @@ export function createDataReader(descriptors: Descriptor[]): DataReader {
326327
switch (descriptor.type) {
327328
// SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder.
328329
case sqlTypes.SQL_VARYING: {
329-
//// TODO: none, octets
330+
// TODO: octets
330331
const varLength = dataView.getUint16(descriptor.offset, littleEndian);
331-
const decoder = new stringDecoder.StringDecoder('utf8');
332+
const encoding = descriptor.charSet === charSets.none ? attachment.charSetForNONE : 'utf8';
332333
const buf = Buffer.from(buffer.buffer, descriptor.offset + 2, varLength);
333-
return decoder.end(buf);
334+
return decodeString(buf, encoding);
334335
}
335336

336337
/***
@@ -527,9 +528,11 @@ export function createDataWriter(descriptors: Descriptor[]): DataWriter {
527528
switch (descriptor.type) {
528529
// SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder.
529530
case sqlTypes.SQL_VARYING: {
530-
//// TODO: none, octets
531+
//// TODO: octets
531532
const str = value as string;
532-
const strBuffer = Buffer.from(str);
533+
const attached = attachment as AbstractAttachment;
534+
const encoding = descriptor.charSet === charSets.none ? attached.charSetForNONE : 'utf8';
535+
const strBuffer = encodeString(str, encoding);
533536

534537
const bytesArray = Uint8Array.from(strBuffer);
535538

packages/node-firebird-driver/src/lib/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,12 @@ export interface ConnectOptions {
4747

4848
/** Set database read/write mode. */
4949
setDatabaseReadWriteMode?: DatabaseReadWriteMode;
50+
51+
/**
52+
* Node.js character set encoding used for Firebird NONE charset columns/parameters.
53+
* Requires iconv-lite package.
54+
*/
55+
charSetForNONE?: string;
5056
}
5157

5258
/** DatabaseReadWriteMode enum */

packages/node-firebird-driver/src/test/tests.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,68 @@ export function runCommonTests(client: Client) {
973973
await attachment.dropDatabase();
974974
});
975975

976+
test('NONE charset uses charSetForNONE for read/write', async () => {
977+
const filename = getTempFile('ResultSet-none-charset.fdb');
978+
979+
{
980+
const attachment = await client.createDatabase(filename);
981+
const transaction = await attachment.startTransaction();
982+
await attachment.execute(transaction, 'create table t1 (id integer, x varchar(10) character set none)');
983+
await transaction.commitRetaining();
984+
await attachment.execute(
985+
transaction,
986+
"insert into t1 (id, x) values (1, cast(x'B99C9F' as varchar(3) character set none))",
987+
);
988+
await attachment.execute(
989+
transaction,
990+
"insert into t1 (id, x) values (2, cast(x'B1B6BC' as varchar(3) character set none))",
991+
);
992+
await transaction.commit();
993+
await attachment.disconnect();
994+
}
995+
996+
{
997+
const attachment = await client.connect(filename);
998+
const transaction = await attachment.startTransaction();
999+
const row1 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1');
1000+
const row2 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2');
1001+
expect(row1[0]).toBe(Buffer.from('b99c9f', 'hex').toString('utf8'));
1002+
expect(row2[0]).toBe(Buffer.from('b1b6bc', 'hex').toString('utf8'));
1003+
await transaction.commit();
1004+
await attachment.disconnect();
1005+
}
1006+
1007+
{
1008+
const attachment = await client.connect(filename, { charSetForNONE: 'windows-1250' });
1009+
const transaction = await attachment.startTransaction();
1010+
const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1');
1011+
expect(row[0]).toBe('ąśź');
1012+
await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [101, 'ąśź']);
1013+
const check = await attachment.executeSingleton(
1014+
transaction,
1015+
"select count(*) from t1 where id = 101 and x = cast(x'B99C9F' as varchar(3) character set none)",
1016+
);
1017+
expect(check[0]).toBe(1);
1018+
await transaction.commit();
1019+
await attachment.disconnect();
1020+
}
1021+
1022+
{
1023+
const attachment = await client.connect(filename, { charSetForNONE: 'iso-8859-2' });
1024+
const transaction = await attachment.startTransaction();
1025+
const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2');
1026+
expect(row[0]).toBe('ąśź');
1027+
await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [201, 'ąśź']);
1028+
const check = await attachment.executeSingleton(
1029+
transaction,
1030+
"select count(*) from t1 where id = 201 and x = cast(x'B1B6BC' as varchar(3) character set none)",
1031+
);
1032+
expect(check[0]).toBe(1);
1033+
await transaction.commit();
1034+
await attachment.dropDatabase();
1035+
}
1036+
});
1037+
9761038
test('#fetch() with fetchSize', async () => {
9771039
const attachment = await client.createDatabase(getTempFile('ResultSet-fetch-with-fetchSize.fdb'));
9781040
const transaction = await attachment.startTransaction();

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6691,6 +6691,11 @@ __metadata:
66916691
resolution: "node-firebird-driver@workspace:packages/node-firebird-driver"
66926692
dependencies:
66936693
"@types/node": "npm:^22.13.10"
6694+
peerDependencies:
6695+
iconv-lite: ^0.7.2
6696+
peerDependenciesMeta:
6697+
iconv-lite:
6698+
optional: true
66946699
languageName: unknown
66956700
linkType: soft
66966701

0 commit comments

Comments
 (0)