Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ node_modules/
.yarn/install-state.gz
dist/

lerna-debug.log
yarn-error.log
*.log
tsconfig.tsbuildinfo
.env
2 changes: 2 additions & 0 deletions packages/node-firebird-driver-native/src/lib/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class AttachmentImpl extends AbstractAttachment {

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

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

return await client.statusAction(async (status) => {
const dpb = createDpb(options);
Expand Down
1 change: 1 addition & 0 deletions packages/node-firebird-driver-native/src/lib/fb-util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function createDescriptors(status: fb.Status, metadata?: fb.MessageMetada
ret.push({
type: metadata.getTypeSync(status, i),
subType: metadata.getSubTypeSync(status, i),
charSet: metadata.getCharSetSync(status, i),
nullOffset: metadata.getNullOffsetSync(status, i),
offset: metadata.getOffsetSync(status, i),
length: metadata.getLengthSync(status, i),
Expand Down
8 changes: 8 additions & 0 deletions packages/node-firebird-driver/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,13 @@
"typings": "./dist/lib/index.d.ts",
"dependencies": {
"@types/node": "^22.13.10"
},
"peerDependencies": {
"iconv-lite": "^0.7.2"
},
"peerDependenciesMeta": {
"iconv-lite": {
"optional": true
}
}
}
1 change: 1 addition & 0 deletions packages/node-firebird-driver/src/lib/impl/attachment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export abstract class AbstractAttachment implements Attachment {
events = new Set<Events>();
statements = new Set<AbstractStatement>();
transactions = new Set<AbstractTransaction>();
charSetForNONE = 'utf8';

/** Default transaction options. */
defaultTransactionOptions: TransactionOptions;
Expand Down
58 changes: 58 additions & 0 deletions packages/node-firebird-driver/src/lib/impl/encoding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
interface IconvLiteModule {
encodingExists(encoding: string): boolean;
decode(buffer: Buffer, encoding: string): string;
encode(value: string, encoding: string): Buffer;
}

let iconvLite: IconvLiteModule | undefined;
let iconvLiteLoadAttempted = false;

function getIconvLite(encoding: string): IconvLiteModule {
if (!iconvLiteLoadAttempted) {
iconvLiteLoadAttempted = true;

try {
// eslint-disable-next-line @typescript-eslint/no-require-imports
iconvLite = require('iconv-lite') as IconvLiteModule;
} catch {
iconvLite = undefined;
}
}

if (!iconvLite) {
throw new Error(
`Encoding '${encoding}' in charSetForNONE requires optional dependency 'iconv-lite'. ` +
`Install it with: yarn add iconv-lite`,
);
}

return iconvLite;
}

export function decodeString(bytes: Buffer, encoding: string): string {
if (Buffer.isEncoding(encoding as BufferEncoding)) {
return bytes.toString(encoding as BufferEncoding);
}

const iconvLiteModule = getIconvLite(encoding);

if (iconvLiteModule.encodingExists(encoding)) {
return iconvLiteModule.decode(bytes, encoding);
}

throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`);
}

export function encodeString(value: string, encoding: string): Buffer {
if (Buffer.isEncoding(encoding as BufferEncoding)) {
return Buffer.from(value, encoding as BufferEncoding);
}

const iconvLiteModule = getIconvLite(encoding);

if (iconvLiteModule.encodingExists(encoding)) {
return iconvLiteModule.encode(value, encoding);
}

throw new Error(`Unknown encoding name '${encoding}' in charSetForNONE option.`);
}
17 changes: 10 additions & 7 deletions packages/node-firebird-driver/src/lib/impl/fb-util.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import * as os from 'os';
const littleEndian = os.endianness() === 'LE';

import * as stringDecoder from 'string_decoder';

import { AbstractAttachment } from './attachment';
import { decodeString, encodeString } from './encoding';
import { decodeDate, decodeTime, encodeDate, encodeTime } from './date-time';
import { tzIdToString, tzStringToId } from './time-zones';
import { AbstractTransaction } from './transaction';
Expand Down Expand Up @@ -129,6 +128,7 @@ export namespace cancelType {
}

export namespace charSets {
export const none = 0;
export const ascii = 2;
}

Expand Down Expand Up @@ -296,6 +296,7 @@ export function getPortableInteger(buffer: Uint8Array, length: number) {
export interface Descriptor {
type: number;
subType: number;
charSet: number;
length: number;
scale: number;
offset: number;
Expand Down Expand Up @@ -326,11 +327,11 @@ export function createDataReader(descriptors: Descriptor[]): DataReader {
switch (descriptor.type) {
// SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder.
case sqlTypes.SQL_VARYING: {
//// TODO: none, octets
// TODO: octets
const varLength = dataView.getUint16(descriptor.offset, littleEndian);
const decoder = new stringDecoder.StringDecoder('utf8');
const encoding = descriptor.charSet === charSets.none ? attachment.charSetForNONE : 'utf8';
const buf = Buffer.from(buffer.buffer, descriptor.offset + 2, varLength);
return decoder.end(buf);
return decodeString(buf, encoding);
}

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

const bytesArray = Uint8Array.from(strBuffer);

Expand Down
6 changes: 6 additions & 0 deletions packages/node-firebird-driver/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export interface ConnectOptions {

/** Set database read/write mode. */
setDatabaseReadWriteMode?: DatabaseReadWriteMode;

/**
* Node.js character set encoding used for Firebird NONE charset columns/parameters.
* Requires iconv-lite package.
*/
charSetForNONE?: string;
}

/** DatabaseReadWriteMode enum */
Expand Down
62 changes: 62 additions & 0 deletions packages/node-firebird-driver/src/test/tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -973,6 +973,68 @@ export function runCommonTests(client: Client) {
await attachment.dropDatabase();
});

test('NONE charset uses charSetForNONE for read/write', async () => {
const filename = getTempFile('ResultSet-none-charset.fdb');

{
const attachment = await client.createDatabase(filename);
const transaction = await attachment.startTransaction();
await attachment.execute(transaction, 'create table t1 (id integer, x varchar(10) character set none)');
await transaction.commitRetaining();
await attachment.execute(
transaction,
"insert into t1 (id, x) values (1, cast(x'B99C9F' as varchar(3) character set none))",
);
await attachment.execute(
transaction,
"insert into t1 (id, x) values (2, cast(x'B1B6BC' as varchar(3) character set none))",
);
await transaction.commit();
await attachment.disconnect();
}

{
const attachment = await client.connect(filename);
const transaction = await attachment.startTransaction();
const row1 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1');
const row2 = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2');
expect(row1[0]).toBe(Buffer.from('b99c9f', 'hex').toString('utf8'));
expect(row2[0]).toBe(Buffer.from('b1b6bc', 'hex').toString('utf8'));
await transaction.commit();
await attachment.disconnect();
}

{
const attachment = await client.connect(filename, { charSetForNONE: 'windows-1250' });
const transaction = await attachment.startTransaction();
const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 1');
expect(row[0]).toBe('ąśź');
await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [101, 'ąśź']);
const check = await attachment.executeSingleton(
transaction,
"select count(*) from t1 where id = 101 and x = cast(x'B99C9F' as varchar(3) character set none)",
);
expect(check[0]).toBe(1);
await transaction.commit();
await attachment.disconnect();
}

{
const attachment = await client.connect(filename, { charSetForNONE: 'iso-8859-2' });
const transaction = await attachment.startTransaction();
const row = await attachment.executeSingleton(transaction, 'select x from t1 where id = 2');
expect(row[0]).toBe('ąśź');
await attachment.execute(transaction, 'insert into t1 (id, x) values (?, ?)', [201, 'ąśź']);
const check = await attachment.executeSingleton(
transaction,
"select count(*) from t1 where id = 201 and x = cast(x'B1B6BC' as varchar(3) character set none)",
);
expect(check[0]).toBe(1);
await transaction.commit();
await attachment.dropDatabase();
}
});

test('#fetch() with fetchSize', async () => {
const attachment = await client.createDatabase(getTempFile('ResultSet-fetch-with-fetchSize.fdb'));
const transaction = await attachment.startTransaction();
Expand Down
5 changes: 5 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6691,6 +6691,11 @@ __metadata:
resolution: "node-firebird-driver@workspace:packages/node-firebird-driver"
dependencies:
"@types/node": "npm:^22.13.10"
peerDependencies:
iconv-lite: ^0.7.2
peerDependenciesMeta:
iconv-lite:
optional: true
languageName: unknown
linkType: soft

Expand Down
Loading