Skip to content

Commit ecf4632

Browse files
authored
Merge branch 'master' into copilot/implement-protocol-16-support
2 parents 99edf30 + cfc9285 commit ecf4632

File tree

15 files changed

+1082
-2152
lines changed

15 files changed

+1082
-2152
lines changed

README.md

Lines changed: 37 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -113,9 +113,9 @@ pool.destroy();
113113

114114
### Database Methods
115115

116-
- `db.query(query, [params], function(err, result))` - classic query, returns Array of Object
117-
- `db.execute(query, [params], function(err, result))` - classic query, returns Array of Array
118-
- `db.sequentially(query, [params], function(row, index), function(err))` - sequentially query
116+
- `db.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
117+
- `db.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
118+
- `db.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
119119
- `db.detach(function(err))` detach a database
120120
- `db.transaction(options, function(err, transaction))` create transaction
121121

@@ -135,11 +135,20 @@ const options = {
135135

136136
### Transaction methods
137137

138-
- `transaction.query(query, [params], function(err, result))` - classic query, returns Array of Object
139-
- `transaction.execute(query, [params], function(err, result))` - classic query, returns Array of Array
138+
- `transaction.query(query, [params], function(err, result), options)` - classic query, returns Array of Object
139+
- `transaction.execute(query, [params], function(err, result), options)` - classic query, returns Array of Array
140+
- `transaction.sequentially(query, [params], function(row, index), function(err), options)` - sequentially query
140141
- `transaction.commit(function(err))` commit current transaction
141142
- `transaction.rollback(function(err))` rollback current transaction
142143

144+
### Statement options
145+
146+
```js
147+
const options = {
148+
timeout: 1000, // Statement timeout in ms, default is 0 (no timeout)
149+
}
150+
```
151+
143152
## Examples
144153

145154
### Parametrized Queries
@@ -683,6 +692,29 @@ Firebird.attach({
683692
- Working with DECFLOAT values as strings or Buffers to preserve exact precision
684693

685694
For legacy Firebird 4 servers with SRP authentication only, use the following configuration in `firebird.conf`:
695+
Firebird 4 wire protocol (versions 16 and 17) is partially supported, including:
696+
- **Time Zone Support**: Native support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE` (Protocol 16+).
697+
- **INT128 support**: Native support for 128-bit integers.
698+
- **Statement Timeout**: Support for statement-level timeouts.
699+
700+
#### Using Timezones (FB 4.0+)
701+
702+
Columns of type `TIMESTAMP WITH TIME ZONE` and `TIME WITH TIME ZONE` are automatically mapped to JavaScript `Date` objects. Values are read as UTC and represented in the local timezone of the Node.js process.
703+
704+
```js
705+
// Select timezone columns
706+
db.query('SELECT TS_TZ_COL, T_TZ_COL FROM FB4_TABLE', function(err, result) {
707+
console.log(result[0].ts_tz_col); // JavaScript Date object
708+
});
709+
710+
// Insert using Date objects
711+
db.query('INSERT INTO FB4_TABLE (TS_TZ_COL) VALUES (?)', [new Date()], function(err) {
712+
// ...
713+
});
714+
```
715+
716+
Srp256 authentication and wire encryption are now supported natively,
717+
so you only need the following minimal configuration in `firebird.conf`:
686718

687719
```bash
688720
AuthServer = Srp256, Srp

Roadmap.md

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ The following table summarizes the current and planned implementation status of
1212
| 3.0 | 14, 15 | ✅ Implemented |
1313
| 4.0 | 16, 17 | ✅ Implemented (Partial) |
1414
| 5.0 | N/A | ❌ Not Implemented |
15+
| 4.0 | 16, 17 | ✅ Protocol 16 Implemented / 🚧 Protocol 17 Missing |
16+
| 5.0 | 18 | ❌ Not Implemented |
1517
| 6.0 | N/A | ❌ Not Implemented |
1618

1719
## Firebird 3 Support
@@ -27,8 +29,6 @@ Firebird 3 introduced Protocol 13, which brought significant changes focusing on
2729
- **UTF-8 User Identification:** ✅ Implemented - all user identification is properly handled with UTF-8 encoding via `isc_dpb_utf8_filename` flag for Firebird 3+.
2830
- **Database Encryption Callback:** ✅ Implemented - support for database encryption key callback (`op_crypt_key_callback`) during the connect phase, allowing connections to encrypted databases. The `dbCryptConfig` connection option supports both plain text and base64-encoded encryption keys.
2931

30-
The following features are planned for future implementation:
31-
3232
## Firebird 4 Support
3333

3434
Firebird 4 introduced Protocol versions 16 and 17, continuing to build upon the foundation of Firebird 3. The following features have been implemented:
@@ -44,23 +44,33 @@ The following features are planned for future implementation:
4444
- **Statement Timeouts:** Implement support for setting maximum execution times for SQL statements at the wire protocol level (Protocol 16 feature).
4545
- **Time Zone Data Types:** Add support for TIMESTAMP WITH TIME ZONE and TIME WITH TIME ZONE types.
4646
- **Full IEEE 754 DECFLOAT Support:** Implement proper IEEE 754 Decimal64 and Decimal128 encoding/decoding for DECFLOAT types (currently uses simplified implementation).
47+
- **Protocol Versions 16 and 17:** ✅ Protocol 16 Implemented - support for statement timeout, INT128, and timezones. 🚧 Protocol 17 Missing.
48+
- **Statement Timeout:** ✅ Implemented - support for `op_execute` with statement timeout (Protocol 16+).
49+
- **`INT128` Data Type:** ✅ Implemented - support for 128-bit integer data type.
50+
- **Time Zone Support:** ✅ Implemented - support for `TIME WITH TIME ZONE`, `TIMESTAMP WITH TIME ZONE`, and `sessionTimeZone` connection option (Protocol 16+).
51+
- **`DECFLOAT` Data Type:** ❌ TODO - support for `DECFLOAT(16)` and `DECFLOAT(34)`.
4752

4853
## Firebird 5 Support
4954

50-
Firebird 5 introduces a host of new SQL features and performance improvements that will require significant client-side implementation:
55+
Firebird 5 introduces Protocol version 18 and a host of new SQL features and performance improvements:
56+
57+
- **Protocol Version 18:** ❌ TODO - implement the latest protocol version.
58+
- **Bidirectional Cursors:** ❌ TODO - implement support for scrollable cursors for remote database access.
59+
- **`RETURNING` Multiple Rows:** ❌ TODO - enhance DML operations to support returning multiple rows.
60+
- **`SKIP LOCKED`:** ❌ TODO - add support for the `SKIP LOCKED` clause in `SELECT WITH LOCK`, `UPDATE`, and `DELETE` statements.
61+
- **Parallel Workers Information:** ❌ TODO - support for parallel workers information in SQL information items.
5162

52-
- **Bidirectional Cursors:** Implement support for scrollable cursors for remote database access.
53-
- **`RETURNING` Multiple Rows:** Enhance DML operations to support returning multiple rows.
54-
- **`SKIP LOCKED`:** Add support for the `SKIP LOCKED` clause in `SELECT WITH LOCK`, `UPDATE`, and `DELETE` statements.
55-
- **New Data Types and Functions:** Add support for new built-in functions and packages.
5663

5764
## Firebird 6 and Beyond
5865

5966
As Firebird 6 and future versions are released, we will continue to monitor new features and plan for their implementation. Key areas of interest include:
6067

61-
- **JSON Support:** Implement client-side support for the new SQL-compliant JSON functions.
62-
- **Tablespaces:** Add support for tablespaces.
63-
- **SQL Schemas:** Implement support for SQL schemas.
68+
- **Native `JSON` Data Type:** Implement support for the new native `JSON` type (often handled as optimized binary storage).
69+
- **SQL-Standard `ROW` Type:** Support for structured data types (records) as columns or variables.
70+
- **SQL-Compliant JSON Functions:** Implement client-side support for `JSON_VALUE`, `JSON_QUERY`, `JSON_EXISTS`, and `JSON_OBJECT`.
71+
- **Tablespaces:** Add support for tablespaces to control physical storage locations.
72+
- **SQL Schemas:** Implement support for SQL-standard schemas for better namespace organization.
73+
- **Enhanced Collation Support:** Support for collations defined directly as part of the data type declaration.
6474

6575
## Codebase Refactoring
6676

lib/index.d.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ declare module 'node-firebird' {
77
type TransactionCallback = (err: any, transaction: Transaction) => void;
88
type QueryCallback = (err: any, result: any[]) => void;
99
type SimpleCallback = (err: any) => void;
10-
type SequentialCallback = (row: any, index: number) => void;
10+
type SequentialCallback = (row: any, index: number, next?: (err?: any) => void) => void | Promise<void>;
1111

1212
export const AUTH_PLUGIN_LEGACY: string;
1313
export const AUTH_PLUGIN_SRP: string;
@@ -41,21 +41,25 @@ declare module 'node-firebird' {
4141
waitTimeout?: number;
4242
};
4343

44+
export type QueryOptions = {
45+
timeout: number;
46+
}
47+
4448
export interface Database {
4549
detach(callback?: SimpleCallback): Database;
4650
transaction(options: TransactionOptions|Isolation|TransactionCallback, callback?: TransactionCallback): Database;
47-
query(query: string, params: any[], callback: QueryCallback): Database;
48-
execute(query: string, params: any[], callback: QueryCallback): Database;
49-
sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, asArray?: boolean): Database;
51+
query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
52+
execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): Database;
53+
sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
5054
drop(callback: SimpleCallback): void;
5155
escape(value: any): string;
5256
attachEvent(callback: any): this;
5357
}
5458

5559
export interface Transaction {
56-
query(query: string, params: any[], callback: QueryCallback): void;
57-
execute(query: string, params: any[], callback: QueryCallback): void;
58-
sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, asArray?: boolean): Database;
60+
query(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
61+
execute(query: string, params: any[], callback: QueryCallback, options?: QueryOptions): void;
62+
sequentially(query: string, params: any[], rowCallback: SequentialCallback, callback: SimpleCallback, options?: QueryOptions | boolean): Database;
5963
commit(callback?: SimpleCallback): void;
6064
commitRetaining(callback?: SimpleCallback): void;
6165
rollback(callback?: SimpleCallback): void;

lib/wire/connection.js

Lines changed: 119 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -408,6 +408,10 @@ class Connection {
408408
if (this.accept.authData) {
409409
blr.addString(Const.isc_dpb_specific_auth_data, this.accept.authData, Const.DEFAULT_ENCODING);
410410
}
411+
412+
if (options.sessionTimeZone) {
413+
blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
414+
}
411415

412416
msg.addInt(Const.op_attach);
413417
msg.addInt(0); // Database Object ID
@@ -508,6 +512,10 @@ class Connection {
508512
if (this.accept.authData) {
509513
blr.addString(Const.isc_dpb_specific_auth_data, this.accept.authData, Const.DEFAULT_ENCODING);
510514
}
515+
516+
if (options.sessionTimeZone) {
517+
blr.addString(Const.isc_dpb_session_time_zone, options.sessionTimeZone, Const.DEFAULT_ENCODING);
518+
}
511519

512520
blr.addNumeric(Const.isc_dpb_sql_dialect, 3);
513521
blr.addNumeric(Const.isc_dpb_force_write, 1);
@@ -1011,6 +1019,10 @@ class Connection {
10111019
case Const.SQL_TYPE_DATE:
10121020
case Const.SQL_TYPE_TIME:
10131021
case Const.SQL_TIMESTAMP:
1022+
case Const.SQL_TIME_TZ:
1023+
case Const.SQL_TIME_TZ_EX:
1024+
case Const.SQL_TIMESTAMP_TZ:
1025+
case Const.SQL_TIMESTAMP_TZ_EX:
10141026
ret[i] = new Xsql.SQLParamDate(null);
10151027
break;
10161028
case Const.SQL_BLOB:
@@ -1034,12 +1046,15 @@ class Connection {
10341046
putBlobData(i, value, done);
10351047
break;
10361048

1037-
case Const.SQL_TIMESTAMP:
1038-
case Const.SQL_TYPE_DATE:
1039-
case Const.SQL_TYPE_TIME:
1040-
1041-
if (value instanceof Date)
1042-
ret[i] = new Xsql.SQLParamDate(value);
1049+
case Const.SQL_TIMESTAMP:
1050+
case Const.SQL_TYPE_DATE:
1051+
case Const.SQL_TYPE_TIME:
1052+
case Const.SQL_TIME_TZ:
1053+
case Const.SQL_TIME_TZ_EX:
1054+
case Const.SQL_TIMESTAMP_TZ:
1055+
case Const.SQL_TIMESTAMP_TZ_EX:
1056+
1057+
if (value instanceof Date) ret[i] = new Xsql.SQLParamDate(value);
10431058
else if (typeof(value) === 'string')
10441059
ret[i] = new Xsql.SQLParamDate(parseDate(value));
10451060
else
@@ -1173,6 +1188,11 @@ class Connection {
11731188
}
11741189
msg.addInt(0); // out_message_number = out_message_type
11751190
}
1191+
1192+
if (this.accept.protocolVersion >= Const.PROTOCOL_VERSION16) {
1193+
// TODO impl statement timout
1194+
msg.addInt(statement.options?.timeout || 0); // p_sqldata_timeout
1195+
}
11761196

11771197
callback.statement = statement;
11781198
this._queueEvent(callback);
@@ -1207,41 +1227,56 @@ class Connection {
12071227

12081228

12091229
fetchAll(statement, transaction, callback) {
1210-
const self = this, data = [];
1230+
const self = this;
1231+
const custom = statement.options || {};
1232+
const asStream = custom.asStream && custom.on;
1233+
const data = asStream ? null : [];
1234+
let streamIndex = 0;
12111235
const loop = (err, ret) => {
12121236
if (err) {
1213-
return callback(err);
1214-
} else if (ret && ret.data && ret.data.length) {
1215-
const arrPromise = (ret.arrBlob || []).map(value => value(transaction));
1216-
1237+
callback(err);
1238+
return;
1239+
}
1240+
1241+
if (ret && ret.data && ret.data.length) {
1242+
const arrPromise = (ret.arrBlob || []).map((value) => value(transaction));
1243+
12171244
Promise.all(arrPromise).then((arrBlob) => {
12181245
for (let i = 0; i < arrBlob.length; i++) {
12191246
const blob = arrBlob[i];
12201247
ret.data[blob.row][blob.column] = blob.value;
12211248
}
1222-
1223-
const lastIndex = ret.data.length - 1;
1224-
for (let i = 0; i < ret.data.length; i++) {
1225-
const pos = data.push(ret.data[i]);
1226-
if (statement.custom && statement.custom.asStream && statement.custom.on) {
1227-
statement.custom.on(ret.data[i], pos - 1);
1249+
1250+
doSynchronousLoop(ret.data, (row, _i, next) => {
1251+
const pos = asStream ? streamIndex++ : (data.push(row) - 1);
1252+
if (asStream) {
1253+
executeStreamRow(custom, row, pos, statement.output, next);
1254+
} else {
1255+
next();
12281256
}
1229-
if (i === lastIndex) {
1230-
if (ret.fetched) {
1231-
return callback(undefined, data);
1232-
} else {
1233-
self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
1234-
}
1257+
}, (streamErr) => {
1258+
if (streamErr) {
1259+
callback(streamErr);
1260+
return;
12351261
}
1236-
}
1262+
1263+
if (ret.fetched) {
1264+
callback(undefined, data || []);
1265+
} else {
1266+
self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
1267+
}
1268+
});
12371269
}).catch(callback);
1238-
} else if (ret.fetched) {
1239-
callback(undefined, data);
1270+
return;
1271+
}
1272+
1273+
if (ret && ret.fetched) {
1274+
callback(undefined, data || []);
12401275
} else {
12411276
self.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
12421277
}
1243-
}
1244-
1278+
};
1279+
12451280
this.fetch(statement, transaction, Const.DEFAULT_FETCHSIZE, loop);
12461281
}
12471282

@@ -1560,7 +1595,7 @@ function decodeResponse(data, callback, cnx, lowercase_keys, cb) {
15601595
case Const.op_sql_response:
15611596
var statement = callback.statement;
15621597
var output = statement.output;
1563-
var custom = statement.custom || {};
1598+
var custom = statement.options || {};
15641599
var isOpFetch = r === Const.op_fetch_response;
15651600
var _xdrpos;
15661601
statement.nbrowsfetched = statement.nbrowsfetched || 0;
@@ -1965,6 +2000,10 @@ function describe(buff, statement) {
19652000
case Const.SQL_TYPE_DATE: param = new Xsql.SQLVarDate(); break;
19662001
case Const.SQL_TYPE_TIME: param = new Xsql.SQLVarTime(); break;
19672002
case Const.SQL_TIMESTAMP: param = new Xsql.SQLVarTimeStamp(); break;
2003+
case Const.SQL_TIME_TZ: param = new Xsql.SQLVarTimeTz(); break;
2004+
case Const.SQL_TIME_TZ_EX: param = new Xsql.SQLVarTimeTzEx(); break;
2005+
case Const.SQL_TIMESTAMP_TZ: param = new Xsql.SQLVarTimeStampTz(); break;
2006+
case Const.SQL_TIMESTAMP_TZ_EX: param = new Xsql.SQLVarTimeStampTzEx(); break;
19682007
case Const.SQL_BLOB: param = new Xsql.SQLVarBlob(); break;
19692008
case Const.SQL_ARRAY: param = new Xsql.SQLVarArray(); break;
19702009
case Const.SQL_QUAD: param = new Xsql.SQLVarQuad(); break;
@@ -2191,6 +2230,57 @@ function fetch_blob_async(statement, id, name, row) {
21912230
};
21922231
}
21932232

2233+
function doSynchronousLoop(data, processData, done) {
2234+
if (!data || !data.length) {
2235+
done();
2236+
return;
2237+
}
2238+
2239+
const loop = (index) => {
2240+
processData(data[index], index, (err) => {
2241+
if (err) {
2242+
done(err);
2243+
return;
2244+
}
2245+
2246+
const nextIndex = index + 1;
2247+
if (nextIndex < data.length) {
2248+
loop(nextIndex);
2249+
} else {
2250+
done();
2251+
}
2252+
});
2253+
};
2254+
2255+
loop(0);
2256+
}
2257+
2258+
function executeStreamRow(custom, row, index, output, next) {
2259+
let done = false;
2260+
const finish = (err) => {
2261+
if (done) {
2262+
return;
2263+
}
2264+
done = true;
2265+
next(err);
2266+
};
2267+
2268+
try {
2269+
const ret = custom.on(row, index, output, finish);
2270+
if (custom.on.length < 4) {
2271+
if (ret && typeof ret.then === 'function') {
2272+
ret.then(() => finish()).catch(finish);
2273+
} else {
2274+
finish();
2275+
}
2276+
} else if (ret && typeof ret.then === 'function') {
2277+
ret.catch(finish);
2278+
}
2279+
} catch (err) {
2280+
finish(err);
2281+
}
2282+
}
2283+
21942284
function bufferReader(buffer, max, writer, cb, beg, end) {
21952285

21962286
if (!beg)

0 commit comments

Comments
 (0)