node-firebird-driver
Version:
Firebird Driver Interfaces for Node.js
597 lines (482 loc) • 17.7 kB
text/typescript
import * as os from 'os';
const littleEndian = os.endianness() === 'LE';
import * as stringDecoder from 'string_decoder';
import { AbstractAttachment } from './attachment';
import { AbstractTransaction } from './transaction';
import { decodeDate, decodeTime, encodeDate, encodeTime } from './date-time';
import { tzIdToString, tzStringToId } from './time-zones';
import {
Attachment,
Blob,
BlobStream,
ConnectOptions,
CreateBlobOptions,
CreateDatabaseOptions,
Transaction,
TransactionIsolation,
TransactionOptions,
ZonedDate,
ZonedDateEx
} from '..';
/** SQL_* type constants */
export namespace sqlTypes {
export const SQL_TEXT = 452;
export const SQL_VARYING = 448;
export const SQL_SHORT = 500;
export const SQL_LONG = 496;
export const SQL_FLOAT = 482;
export const SQL_DOUBLE = 480;
//export const SQL_D_FLOAT = 530;
export const SQL_TIMESTAMP = 510;
export const SQL_BLOB = 520;
//export const SQL_ARRAY = 540;
//export const SQL_QUAD = 550;
export const SQL_TYPE_TIME = 560;
export const SQL_TYPE_DATE = 570;
export const SQL_INT64 = 580;
export const SQL_TIMESTAMP_TZ_EX = 32748;
export const SQL_TIME_TZ_EX = 32750;
export const SQL_TIMESTAMP_TZ = 32754;
export const SQL_TIME_TZ = 32756;
export const SQL_INT128 = 32752;
export const SQL_DEC16 = 32760;
export const SQL_DEC34 = 32762;
export const SQL_BOOLEAN = 32764;
export const SQL_NULL = 32766;
}
/** DPB constants. */
export namespace dpb {
/* tslint:disable */
export const version1 = 1;
export const lc_ctype = 48;
export const force_write = 24;
export const user_name = 28;
export const password = 29;
export const sql_role_name = 60;
/* tslint:enable */
}
/** TPB constants. */
export namespace tpb {
/* tslint:disable */
export const version1 = 1;
export const consistency = 1;
export const concurrency = 2;
export const wait = 6;
export const nowait = 7;
export const read = 8;
export const write = 9;
export const ignore_limbo = 14;
export const read_committed = 15;
export const autocommit = 16;
export const rec_version = 17;
export const no_rec_version = 18;
export const restart_requests = 19;
export const no_auto_undo = 20;
/* tslint:enable */
}
/** BPB constants. */
export namespace bpb {
/* tslint:disable */
export const version1 = 1;
export const source_type = 1;
export const target_type = 2;
export const type = 3;
export const source_interp = 4;
export const target_interp = 5;
export const filter_parameter = 6;
export const storage = 7;
export const type_segmented = 0x0;
export const type_stream = 0x1;
export const storage_main = 0x0;
export const storage_temp = 0x2;
/* tslint:enable */
}
/** EPB constants. */
export namespace epb {
/* tslint:disable */
export const version1 = 1;
/* tslint:enable */
}
/** Blob info. */
export namespace blobInfo {
export const totalLength = 6;
}
/** Statement info. */
export namespace statementInfo {
export const sqlExecPathBlrText = 32;
}
/** Common info. */
export namespace commonInfo {
export const end = 1;
export const truncated = 2;
export const error = 3;
export const dataNotReady = 4;
export const length = 126;
export const flagEnd = 127;
}
export namespace cancelType {
export const disable = 1;
export const enable = 2;
export const raise = 3;
export const abort = 4;
}
export namespace charSets {
export const ascii = 2;
}
export function createDpb(options?: ConnectOptions | CreateDatabaseOptions): Buffer {
const code = (c: number) => String.fromCharCode(c);
const charSet = 'utf8';
let ret = `${code(dpb.version1)}${code(dpb.lc_ctype)}${code(charSet.length)}${charSet}`;
if (!options)
options = {};
if (!options.username)
options.username = process.env.ISC_USER;
if (!options.password)
options.password = process.env.ISC_PASSWORD;
if (options.username)
ret += `${code(dpb.user_name)}${code(options.username.length)}${options.username}`;
if (options.password)
ret += `${code(dpb.password)}${code(options.password.length)}${options.password}`;
if (options.role)
ret += `${code(dpb.sql_role_name)}${code(options.role.length)}${options.role}`;
const createOptions = options as CreateDatabaseOptions;
if (createOptions.forcedWrite != undefined)
ret += `${code(dpb.force_write)}${code(1)}${code(createOptions.forcedWrite ? 1 : 0)}`;
return Buffer.from(ret);
}
export function createTpb(options?: TransactionOptions): Buffer {
const code = (c: number) => String.fromCharCode(c);
let ret = code(tpb.version1);
if (!options)
options = {};
switch (options.accessMode) {
case 'READ_ONLY':
ret += code(tpb.read);
break;
case 'READ_WRITE':
ret += code(tpb.write);
break;
}
switch (options.waitMode) {
case 'NO_WAIT':
ret += code(tpb.nowait);
break;
case 'WAIT':
ret += code(tpb.wait);
break;
}
switch (options.isolation) {
case TransactionIsolation.CONSISTENCY:
ret += code(tpb.consistency);
break;
case TransactionIsolation.SNAPSHOT:
ret += code(tpb.concurrency);
break;
case TransactionIsolation.READ_COMMITTED:
ret += code(tpb.read_committed) +
code(options.readCommittedMode == 'RECORD_VERSION' ? tpb.rec_version : tpb.no_rec_version);
break;
}
if (options.noAutoUndo)
ret += code(tpb.no_auto_undo);
if (options.ignoreLimbo)
ret += code(tpb.ignore_limbo);
if (options.restartRequests)
ret += code(tpb.restart_requests);
if (options.autoCommit)
ret += code(tpb.autocommit);
return Buffer.from(ret);
}
export function createBpb(options?: CreateBlobOptions): Buffer {
const code = (c: number) => String.fromCharCode(c);
let ret = code(bpb.version1);
if (!options)
options = {};
switch (options.type) {
case 'SEGMENTED':
ret += `${code(bpb.type)}${code(1)}${code(bpb.type_segmented)}`;
break;
case 'STREAM':
ret += `${code(bpb.type)}${code(1)}${code(bpb.type_stream)}`;
break;
}
return Buffer.from(ret);
}
/** Changes a number from a scale to another. */
/***
export function changeScale(value: number, inputScale: number, outputScale: number): number {
outputScale -= inputScale;
Math.pow(10, outputScale);
if (outputScale === 0)
return value;
else if (outputScale > 0)
return value / Math.pow(10, outputScale);
else // outputScale < 0
return value * Math.pow(10, -outputScale);
}
***/
/** Emulate Firebird isc_portable_integer. */
export function getPortableInteger(buffer: Uint8Array, length: number) {
if (!buffer || length <= 0 || length > 8)
return 0;
let value = 0;
let pos = 0;
for (let shift = 0; --length >= 0; shift += 8)
value += buffer[pos++] << shift;
return value;
}
/** Descriptor for a field or parameter. */
export interface Descriptor {
type: number;
subType: number;
length: number;
scale: number;
offset: number;
nullOffset: number;
}
export type DataReader = (attachment: Attachment, transaction: Transaction, buffer: Uint8Array) => Promise<any[]>;
export type ItemReader = (attachment: Attachment, transaction: Transaction, buffer: Uint8Array) => Promise<any>;
/** Creates a data reader. */
export function createDataReader(descriptors: Descriptor[]): DataReader {
const mappers = new Array<ItemReader>(descriptors.length);
for (let i = 0; i < descriptors.length; ++i) {
const descriptor = descriptors[i];
mappers[i] = async (attachment: AbstractAttachment, transaction: AbstractTransaction, buffer: Uint8Array): Promise<any> => {
const dataView = new DataView(buffer.buffer);
if (dataView.getInt16(descriptor.nullOffset, littleEndian) == -1)
return null;
switch (descriptor.type) {
// SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder.
case sqlTypes.SQL_VARYING: {
//// TODO: none, octets
const varLength = dataView.getUint16(descriptor.offset, littleEndian);
const decoder = new stringDecoder.StringDecoder('utf8');
const buf = Buffer.from(buffer.buffer, descriptor.offset + 2, varLength);
return decoder.end(buf);
}
/***
case sqlTypes.SQL_SHORT:
return changeScale(dataView.getInt16(descriptor.offset, littleEndian), descriptor.scale, 0);
case sqlTypes.SQL_LONG:
return changeScale(dataView.getInt32(descriptor.offset, littleEndian), descriptor.scale, 0);
//// TODO: sqlTypes.SQL_INT64
case sqlTypes.SQL_FLOAT:
return dataView.getFloat32(descriptor.offset, littleEndian);
***/
case sqlTypes.SQL_DOUBLE:
return dataView.getFloat64(descriptor.offset, littleEndian);
case sqlTypes.SQL_TYPE_TIME: {
const now = new Date();
const decodedTime = decodeTime(dataView.getUint32(descriptor.offset, littleEndian));
return new Date(now.getFullYear(), now.getMonth(), now.getDate(),
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10);
}
case sqlTypes.SQL_TIME_TZ_EX: {
const now = new Date();
const decodedTime = decodeTime(dataView.getUint32(descriptor.offset, littleEndian));
const date = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10));
const timeZone = tzIdToString(dataView.getUint16(descriptor.offset + 4, littleEndian));
const offset = dataView.getInt16(descriptor.offset + 6, littleEndian);
return {
date,
timeZone,
offset
} as ZonedDateEx;
}
case sqlTypes.SQL_TYPE_DATE: {
const decodedDate = decodeDate(dataView.getInt32(descriptor.offset, littleEndian));
if (decodedDate.year >= 100)
return new Date(decodedDate.year, decodedDate.month - 1, decodedDate.day);
else {
const date = new Date(2000, decodedDate.month - 1, decodedDate.day);
date.setFullYear(decodedDate.year);
return date;
}
}
case sqlTypes.SQL_TIMESTAMP: {
const decodedDate = decodeDate(dataView.getInt32(descriptor.offset, littleEndian));
const decodedTime = decodeTime(dataView.getUint32(descriptor.offset + 4, littleEndian));
if (decodedDate.year >= 100) {
return new Date(decodedDate.year, decodedDate.month - 1, decodedDate.day,
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10);
}
else {
const date = new Date(2000, decodedDate.month - 1, decodedDate.day,
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10);
date.setFullYear(decodedDate.year);
return date;
}
}
case sqlTypes.SQL_TIMESTAMP_TZ_EX: {
const decodedDate = decodeDate(dataView.getInt32(descriptor.offset, littleEndian));
const decodedTime = decodeTime(dataView.getUint32(descriptor.offset + 4, littleEndian));
const timeZone = tzIdToString(dataView.getUint16(descriptor.offset + 8, littleEndian));
const offset = dataView.getInt16(descriptor.offset + 10, littleEndian);
let date: Date;
if (decodedDate.year >= 100) {
date = new Date(Date.UTC(decodedDate.year, decodedDate.month - 1, decodedDate.day,
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10));
}
else {
date = new Date(Date.UTC(2000, decodedDate.month - 1, decodedDate.day,
decodedTime.hours, decodedTime.minutes, decodedTime.seconds, decodedTime.fractions / 10));
date.setUTCFullYear(decodedDate.year);
}
return {
date,
timeZone,
offset
} as ZonedDateEx;
}
case sqlTypes.SQL_BOOLEAN:
return dataView.getInt8(descriptor.offset) != 0;
case sqlTypes.SQL_BLOB:
return new Blob(attachment, buffer.slice(descriptor.offset, descriptor.offset + 8));
case sqlTypes.SQL_NULL:
return null;
default:
throw new Error(`Unrecognized Firebird type number ${descriptor.type}`);
}
};
}
return async (attachment: Attachment, transaction: Transaction, buffer: Uint8Array): Promise<any[]> => {
return await Promise.all(mappers.map(mapper => mapper(attachment, transaction, buffer)));
};
}
export type DataWriter = (attachment: Attachment, transaction: Transaction,
buffer: Uint8Array, values: Array<any> | undefined) => Promise<void>;
export type ItemWriter = (attachment: Attachment, transaction: Transaction, buffer: Uint8Array, values: any) => Promise<void>;
/** Creates a data writer. */
export function createDataWriter(descriptors: Descriptor[]): DataWriter {
const mappers = new Array<ItemWriter>(descriptors.length);
for (let i = 0; i < descriptors.length; ++i) {
const descriptor = descriptors[i];
mappers[i] = async (attachment: Attachment, transaction: Transaction, buffer: Uint8Array, value: any): Promise<void> => {
const dataView = new DataView(buffer.buffer);
if (value == null) {
dataView.setInt16(descriptor.nullOffset, -1, littleEndian);
return;
}
dataView.setInt16(descriptor.nullOffset, 0, littleEndian);
switch (descriptor.type) {
// SQL_TEXT is handled changing its descriptor to SQL_VARYING with IMetadataBuilder.
case sqlTypes.SQL_VARYING: {
//// TODO: none, octets
const str = value as string;
const strBuffer = Buffer.from(str);
const bytesArray = Uint8Array.from(strBuffer);
if (bytesArray.length > descriptor.length) {
throw new Error(`Length in bytes of string '${str}' (${bytesArray.length}) is ` +
`greater than maximum expect length ${descriptor.length}.`);
}
dataView.setUint16(descriptor.offset, bytesArray.length, littleEndian);
for (let j = 0; j < bytesArray.length; ++j)
buffer[descriptor.offset + 2 + j] = bytesArray[j];
break;
}
/***
case sqlTypes.SQL_SHORT:
dataView.setInt16(descriptor.offset, changeScale(value, 0, descriptor.scale), littleEndian);
break;
case sqlTypes.SQL_LONG:
dataView.setInt32(descriptor.offset, changeScale(value, 0, descriptor.scale), littleEndian);
break;
//// TODO: sqlTypes.SQL_INT64
case sqlTypes.SQL_FLOAT:
dataView.setFloat32(descriptor.offset, value, littleEndian);
break;
***/
case sqlTypes.SQL_DOUBLE:
dataView.setFloat64(descriptor.offset, value, littleEndian);
break;
case sqlTypes.SQL_TYPE_TIME: {
const date = value as Date;
dataView.setUint32(descriptor.offset,
encodeTime(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds() * 10),
littleEndian);
break;
}
case sqlTypes.SQL_TIME_TZ:
case sqlTypes.SQL_TIME_TZ_EX: {
const zonedDate = value as ZonedDate;
dataView.setUint32(descriptor.offset,
encodeTime(zonedDate.date.getUTCHours(), zonedDate.date.getUTCMinutes(), zonedDate.date.getUTCSeconds(),
zonedDate.date.getUTCMilliseconds() * 10),
littleEndian);
dataView.setUint16(descriptor.offset + 4, tzStringToId(zonedDate.timeZone), littleEndian);
break;
}
case sqlTypes.SQL_TYPE_DATE: {
const date = value as Date;
dataView.setInt32(descriptor.offset,
encodeDate(date.getFullYear(), date.getMonth() + 1, date.getDate()),
littleEndian);
break;
}
case sqlTypes.SQL_TIMESTAMP: {
const date = value as Date;
dataView.setInt32(descriptor.offset,
encodeDate(date.getFullYear(), date.getMonth() + 1, date.getDate()),
littleEndian);
dataView.setUint32(descriptor.offset + 4,
encodeTime(date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds() * 10),
littleEndian);
break;
}
case sqlTypes.SQL_TIMESTAMP_TZ:
case sqlTypes.SQL_TIMESTAMP_TZ_EX: {
const zonedDate = value as ZonedDate;
dataView.setInt32(descriptor.offset,
encodeDate(zonedDate.date.getUTCFullYear(), zonedDate.date.getUTCMonth() + 1, zonedDate.date.getUTCDate()),
littleEndian);
dataView.setUint32(descriptor.offset + 4,
encodeTime(zonedDate.date.getUTCHours(), zonedDate.date.getUTCMinutes(), zonedDate.date.getUTCSeconds(),
zonedDate.date.getUTCMilliseconds() * 10),
littleEndian);
dataView.setUint16(descriptor.offset + 8, tzStringToId(zonedDate.timeZone), littleEndian);
break;
}
case sqlTypes.SQL_BOOLEAN:
dataView.setInt8(descriptor.offset, value ? 1 : 0);
break;
case sqlTypes.SQL_BLOB:
{
const targetBlobId = buffer.subarray(descriptor.offset, descriptor.offset + 8);
if (value instanceof BlobStream)
value = value.blob;
if (value instanceof Buffer) {
const blobStream = await attachment.createBlob(transaction);
try {
await blobStream.write(value);
}
catch (e) {
await blobStream.cancel();
throw e;
}
await blobStream.close();
targetBlobId.set(blobStream.blob.id);
}
else if (value instanceof Blob) {
if (value.attachment == attachment)
targetBlobId.set(value.id);
else
throw new Error('Cannot pass a BLOB from another attachment as parameter.'); //// TODO: add support for it
}
else
throw new Error('Unrecognized type used as BLOB. Must be: Buffer or Blob.');
break;
}
case sqlTypes.SQL_NULL:
break;
default:
throw new Error(`Unrecognized Firebird type number ${descriptor.type}`);
}
};
}
return async (attachment: Attachment, transaction: Transaction, buffer: Uint8Array, values: Array<any>): Promise<void> => {
if ((values || []).length !== descriptors.length)
throw new Error(`Incorrect number of parameters: expected ${descriptors.length}, received ${(values || []).length}.`);
await Promise.all(mappers.map((mapper, index) => mapper(attachment, transaction, buffer, values[index])));
};
}