Skip to content

Commit 40ed8b6

Browse files
authored
Merge pull request #393 from hgourvest/timezone
add Time Zone Support from Firebird 4.0
2 parents 7e02c57 + d84c02b commit 40ed8b6

File tree

8 files changed

+344
-10
lines changed

8 files changed

+344
-10
lines changed

README.md

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -654,8 +654,28 @@ Firebird.attach({
654654

655655
### Firebird 4.0 and 5.0
656656

657-
Firebird 4 wire protocol (versions 16 and 17) is not supported yet.
658-
However, Srp256 authentication and wire encryption are now supported natively,
657+
Firebird 4 wire protocol (versions 16 and 17) is partially supported, including:
658+
- **Time Zone Support**: Native support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE` (Protocol 16+).
659+
- **INT128 support**: Native support for 128-bit integers.
660+
- **Statement Timeout**: Support for statement-level timeouts.
661+
662+
#### Using Timezones (FB 4.0+)
663+
664+
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.
665+
666+
```js
667+
// Select timezone columns
668+
db.query('SELECT TS_TZ_COL, T_TZ_COL FROM FB4_TABLE', function(err, result) {
669+
console.log(result[0].ts_tz_col); // JavaScript Date object
670+
});
671+
672+
// Insert using Date objects
673+
db.query('INSERT INTO FB4_TABLE (TS_TZ_COL) VALUES (?)', [new Date()], function(err) {
674+
// ...
675+
});
676+
```
677+
678+
Srp256 authentication and wire encryption are now supported natively,
659679
so you only need the following minimal configuration in `firebird.conf`:
660680

661681
```bash

Roadmap.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,8 @@ Firebird 4 introduced Protocol versions 16 and 17, continuing to build upon the
3434
- **Protocol Versions 16 and 17:** 🚧 Partially Implemented - Protocol 16 is defined, but Protocol 17 is missing.
3535
- **Statement Timeout:** ✅ Implemented - support for `op_execute` with statement timeout (Protocol 16+).
3636
- **`INT128` Data Type:** ✅ Implemented - support for 128-bit integer data type.
37+
- **Time Zone Support:** ✅ Implemented - support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE` (Protocol 16+).
3738
- **`DECFLOAT` Data Type:** ❌ TODO - support for `DECFLOAT(16)` and `DECFLOAT(34)`.
38-
- **Time Zone Support:** ❌ TODO - support for `TIME WITH TIME ZONE` and `TIMESTAMP WITH TIME ZONE`.
3939

4040
## Firebird 5 Support
4141

lib/wire/connection.js

Lines changed: 25 additions & 6 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
@@ -1985,6 +2000,10 @@ function describe(buff, statement) {
19852000
case Const.SQL_TYPE_DATE: param = new Xsql.SQLVarDate(); break;
19862001
case Const.SQL_TYPE_TIME: param = new Xsql.SQLVarTime(); break;
19872002
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;
19882007
case Const.SQL_BLOB: param = new Xsql.SQLVarBlob(); break;
19892008
case Const.SQL_ARRAY: param = new Xsql.SQLVarArray(); break;
19902009
case Const.SQL_QUAD: param = new Xsql.SQLVarQuad(); break;

lib/wire/const.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ const sqlType = {
246246
SQL_INT128: 32752, // >= 4.0
247247
SQL_BOOLEAN : 32764, // >: 3.0
248248
SQL_NULL : 32766, // >= 2.5
249+
SQL_TIMESTAMP_TZ_EX : 32748,
250+
SQL_TIME_TZ_EX : 32750,
251+
SQL_TIMESTAMP_TZ : 32754,
252+
SQL_TIME_TZ : 32756,
249253
};
250254

251255
const blobType = {
@@ -283,6 +287,11 @@ const blr = {
283287
blr_column_name2 : 22, // >: 2.5
284288
blr_bool : 23, // >: 3.0
285289

290+
blr_sql_time_tz : 28, // >: 4.0
291+
blr_timestamp_tz : 29, // >: 4.0
292+
blr_ex_time_tz : 30, // >: 4.0
293+
blr_ex_timestamp_tz : 31, // >: 4.0
294+
286295
blr_version4 : 4,
287296
blr_version5 : 5, // dialect 3
288297
blr_eoc : 76,

lib/wire/xsqlvar.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,6 +295,92 @@ class SQLVarTimeStamp {
295295

296296
//------------------------------------------------------
297297

298+
class SQLVarTimeTz {
299+
decode(data, lowerV13) {
300+
var time = data.readUInt();
301+
data.readInt(); // skip timezone info
302+
303+
if (!lowerV13 || !data.readInt()) {
304+
var d = new Date(0);
305+
d.setMilliseconds(Math.floor(time / 10) + d.getTimezoneOffset() * MsPerMinute);
306+
return d;
307+
}
308+
return null;
309+
}
310+
311+
calcBlr(blr) {
312+
blr.addByte(Const.blr_sql_time_tz);
313+
}
314+
}
315+
316+
//------------------------------------------------------
317+
318+
class SQLVarTimeTzEx extends SQLVarTimeTz {
319+
decode(data, lowerV13) {
320+
var time = data.readUInt();
321+
data.readInt(); // skip timezone info
322+
data.readInt(); // skip ext_offset
323+
324+
if (!lowerV13 || !data.readInt()) {
325+
var d = new Date(0);
326+
d.setMilliseconds(Math.floor(time / 10) + d.getTimezoneOffset() * MsPerMinute);
327+
return d;
328+
}
329+
return null;
330+
}
331+
332+
calcBlr(blr) {
333+
blr.addByte(Const.blr_ex_time_tz);
334+
}
335+
}
336+
337+
//------------------------------------------------------
338+
339+
class SQLVarTimeStampTz {
340+
decode(data, lowerV13) {
341+
var date = data.readInt();
342+
var time = data.readUInt();
343+
data.readInt(); // skip timezone info
344+
345+
if (!lowerV13 || !data.readInt()) {
346+
var d = new Date(0);
347+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10) + d.getTimezoneOffset() * MsPerMinute);
348+
return d;
349+
}
350+
351+
return null;
352+
}
353+
354+
calcBlr(blr) {
355+
blr.addByte(Const.blr_timestamp_tz);
356+
}
357+
}
358+
359+
//------------------------------------------------------
360+
361+
class SQLVarTimeStampTzEx extends SQLVarTimeStampTz {
362+
decode(data, lowerV13) {
363+
var date = data.readInt();
364+
var time = data.readUInt();
365+
data.readInt(); // skip timezone info
366+
data.readInt(); // skip ext_offset
367+
368+
if (!lowerV13 || !data.readInt()) {
369+
var d = new Date(0);
370+
d.setMilliseconds((date - DateOffset) * TimeCoeff + Math.floor(time / 10) + d.getTimezoneOffset() * MsPerMinute);
371+
return d;
372+
}
373+
374+
return null;
375+
}
376+
377+
calcBlr(blr) {
378+
blr.addByte(Const.blr_ex_timestamp_tz);
379+
}
380+
}
381+
382+
//------------------------------------------------------
383+
298384
class SQLVarBoolean {
299385
decode(data, lowerV13) {
300386
var ret = data.readInt();
@@ -516,6 +602,10 @@ module.exports = {
516602
SQLVarText,
517603
SQLVarTime,
518604
SQLVarTimeStamp,
605+
SQLVarTimeTz,
606+
SQLVarTimeTzEx,
607+
SQLVarTimeStampTz,
608+
SQLVarTimeStampTzEx,
519609
SQLParamBool,
520610
SQLParamDate,
521611
SQLParamDouble,

test/timezone-integration.js

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
const Firebird = require('../lib');
2+
const Config = require('./config');
3+
const assert = require('assert');
4+
5+
const config = Config.default;
6+
7+
function fromCallback(executor) {
8+
return new Promise((resolve, reject) => {
9+
executor((err, result) => err ? reject(err) : resolve(result));
10+
});
11+
}
12+
13+
describe('Timezone Support (Firebird 4.0+)', () => {
14+
let db;
15+
let supportsTimezone = false;
16+
17+
beforeAll(async () => {
18+
try {
19+
db = await fromCallback(cb => Firebird.attach(config, cb));
20+
// Check if server supports timezone types
21+
await fromCallback(cb => db.query('SELECT CAST('12:00:00 UTC' AS TIME WITH TIME ZONE) FROM RDB$DATABASE', cb));
22+
supportsTimezone = true;
23+
} catch (err) {
24+
console.warn('Firebird server does not support Timezone types, skipping integration tests.');
25+
if (db) {
26+
await fromCallback(cb => db.detach(cb));
27+
db = null;
28+
}
29+
}
30+
});
31+
32+
afterAll(async () => {
33+
if (db) {
34+
await fromCallback(cb => db.detach(cb));
35+
}
36+
});
37+
38+
it('should select TIME WITH TIME ZONE and TIMESTAMP WITH TIME ZONE', { skip: !supportsTimezone }, async () => {
39+
const query = `
40+
SELECT
41+
CAST('12:00:00 UTC' AS TIME WITH TIME ZONE) as T_TZ,
42+
CAST('2024-01-01 12:00:00 UTC' AS TIMESTAMP WITH TIME ZONE) as TS_TZ
43+
FROM RDB$DATABASE
44+
`;
45+
const rows = await fromCallback(cb => db.query(query, cb));
46+
const row = rows[0];
47+
48+
assert.ok(row.t_tz instanceof Date, 'TIME WITH TIME ZONE should be a Date');
49+
assert.ok(row.ts_tz instanceof Date, 'TIMESTAMP WITH TIME ZONE should be a Date');
50+
51+
// Firebird returns these in UTC, we represent them in local time
52+
// 12:00:00 UTC should have 12 hours in UTC
53+
assert.strictEqual(row.t_tz.getUTCHours(), 12);
54+
assert.strictEqual(row.ts_tz.getUTCFullYear(), 2024);
55+
assert.strictEqual(row.ts_tz.getUTCHours(), 12);
56+
});
57+
58+
it('should round-trip TIME WITH TIME ZONE and TIMESTAMP WITH TIME ZONE', { skip: !supportsTimezone }, async () => {
59+
const table_sql = 'CREATE TABLE TEST_TZ (ID INT, T_TZ TIME WITH TIME ZONE, TS_TZ TIMESTAMP WITH TIME ZONE)';
60+
await fromCallback(cb => db.query(table_sql, cb));
61+
62+
try {
63+
const now = new Date();
64+
// Reset milliseconds for accurate comparison as Firebird might have different precision
65+
now.setMilliseconds(0);
66+
67+
await fromCallback(cb => db.query(
68+
'INSERT INTO TEST_TZ (ID, T_TZ, TS_TZ) VALUES (?, ?, ?)',
69+
[1, now, now],
70+
cb
71+
));
72+
73+
const rows = await fromCallback(cb => db.query('SELECT T_TZ, TS_TZ FROM TEST_TZ WHERE ID = 1', cb));
74+
const row = rows[0];
75+
76+
// Compare UTC times to avoid timezone offset issues during comparison
77+
assert.strictEqual(row.ts_tz.getTime(), now.getTime(), 'Timestamp round-trip mismatch');
78+
79+
// For TIME, we only compare the time part (hours, minutes, seconds)
80+
assert.strictEqual(row.t_tz.getUTCHours(), now.getUTCHours());
81+
assert.strictEqual(row.t_tz.getUTCMinutes(), now.getUTCMinutes());
82+
assert.strictEqual(row.t_tz.getUTCSeconds(), now.getUTCSeconds());
83+
84+
} finally {
85+
await fromCallback(cb => db.query('DROP TABLE TEST_TZ', cb)).catch(() => {});
86+
}
87+
});
88+
89+
it('should handle sessionTimeZone option', { skip: !supportsTimezone }, async () => {
90+
const utcConfig = Object.assign({}, config, { sessionTimeZone: 'UTC' });
91+
const utcDb = await fromCallback(cb => Firebird.attach(utcConfig, cb));
92+
try {
93+
const rows = await fromCallback(cb => utcDb.query('SELECT CURRENT_TIMESTAMP FROM RDB$DATABASE', cb));
94+
assert.ok(rows[0].current_timestamp instanceof Date);
95+
} finally {
96+
await fromCallback(cb => utcDb.detach(cb));
97+
}
98+
});
99+
});

0 commit comments

Comments
 (0)